From b667644100a3e0831b0f51ceab853be3e29703bd Mon Sep 17 00:00:00 2001
From: "@mrubens" <2600+mrubens@users.noreply.github.com>
Date: Sun, 16 Aug 2026 21:49:22 +0000
Subject: [PATCH 1/4] feat: add per-user Slack fast mode default
---
.env.production.example | 2 +
.../slack/__tests__/fast-agent.test.ts | 41 +++++++++
.../src/handlers/slack/events/fast-agent.ts | 16 ++++
.../handlers/slack/events/message-entry.ts | 13 ++-
.../slack/helpers/user-mapping.test.ts | 4 +
.../handlers/slack/helpers/user-mapping.ts | 9 +-
apps/docs/environment-variables.mdx | 1 +
.../settings/UserPreferencesSection.test.tsx | 83 ++++++++++++++-----
.../settings/UserPreferencesSection.tsx | 39 ++++++++-
.../pages/PersonalSettingsPage.test.tsx | 2 +
.../settings/pages/PersonalSettingsPage.tsx | 6 +-
.../pages/PersonalSettingsRoute.test.tsx | 14 +++-
.../settings/pages/PersonalSettingsRoute.tsx | 3 +
.../usePersonalPreferences.client.test.tsx | 1 +
apps/web/src/hooks/usePersonalPreferences.ts | 10 +++
.../src/trpc/commands/preferences/index.ts | 19 +++++
.../preferences/personal-preferences.test.ts | 54 ++++++++++++
apps/web/src/trpc/routers/_app.ts | 4 +-
apps/web/src/types/preferences.ts | 2 +
docker-compose.production.yml | 1 +
docker-compose.self-host.yml | 1 +
packages/env/src/__tests__/index.test.ts | 16 ++++
packages/env/src/index.ts | 2 +
23 files changed, 313 insertions(+), 30 deletions(-)
diff --git a/.env.production.example b/.env.production.example
index 5a352cad1..934c9fcd5 100644
--- a/.env.production.example
+++ b/.env.production.example
@@ -154,6 +154,8 @@ DEFAULT_COMPUTE_PROVIDER=docker
# Set to true to prevent curated integrations from being configured or used.
# Existing connections remain stored and become available again once unset.
# R_CURATED_INTEGRATIONS_DISABLED=true
+# Set to true to let users default their Slack messages to fast mode.
+# R_SLACK_FAST_MODE_SETTING_ENABLED=true
# SLACK_APP_ID=
# R_SLACK_SIGNING_SECRET=
# R_TELEGRAM_BOT_TOKEN=
diff --git a/apps/api/src/handlers/slack/__tests__/fast-agent.test.ts b/apps/api/src/handlers/slack/__tests__/fast-agent.test.ts
index 04f5e01b7..ce1e7f55d 100644
--- a/apps/api/src/handlers/slack/__tests__/fast-agent.test.ts
+++ b/apps/api/src/handlers/slack/__tests__/fast-agent.test.ts
@@ -1,6 +1,7 @@
import {
extractFastQuestion,
isFastCommandInvocation,
+ resolveFastAgentEntryMode,
stripLeadingFastCommandMention,
} from '../events/fast-agent';
@@ -26,4 +27,44 @@ describe('Slack fast-agent helpers', () => {
expect(extractFastQuestion(' ', true)).toBeNull();
expect(extractFastQuestion('Good, tired')).toBeNull();
});
+
+ it('keeps ordinary messages on standard routing when the deployment flag is disabled', () => {
+ expect(
+ resolveFastAgentEntryMode({
+ text: 'please fix this',
+ deploymentSettingEnabled: false,
+ userDefaultEnabled: true,
+ }),
+ ).toBeNull();
+ });
+
+ it('keeps ordinary messages on standard routing when the user setting is off', () => {
+ expect(
+ resolveFastAgentEntryMode({
+ text: 'please fix this',
+ deploymentSettingEnabled: true,
+ userDefaultEnabled: false,
+ }),
+ ).toBeNull();
+ });
+
+ it('defaults ordinary messages to fast mode when both settings are enabled', () => {
+ expect(
+ resolveFastAgentEntryMode({
+ text: 'please fix this',
+ deploymentSettingEnabled: true,
+ userDefaultEnabled: true,
+ }),
+ ).toBe('default');
+ });
+
+ it('preserves explicit !fast routing regardless of the user setting', () => {
+ expect(
+ resolveFastAgentEntryMode({
+ text: '!fast please fix this',
+ deploymentSettingEnabled: true,
+ userDefaultEnabled: true,
+ }),
+ ).toBe('explicit');
+ });
});
diff --git a/apps/api/src/handlers/slack/events/fast-agent.ts b/apps/api/src/handlers/slack/events/fast-agent.ts
index 1024d477b..04f49efc4 100644
--- a/apps/api/src/handlers/slack/events/fast-agent.ts
+++ b/apps/api/src/handlers/slack/events/fast-agent.ts
@@ -27,6 +27,22 @@ export function isBareFastCommandInvocation(text: string): boolean {
return /^!fast(?:\s|$)/i.test(text.trimStart());
}
+export type FastAgentEntryMode = 'explicit' | 'default';
+
+export function resolveFastAgentEntryMode(params: {
+ text: string;
+ deploymentSettingEnabled: boolean;
+ userDefaultEnabled: boolean;
+}): FastAgentEntryMode | null {
+ if (isFastCommandInvocation(params.text)) {
+ return 'explicit';
+ }
+
+ return params.deploymentSettingEnabled && params.userDefaultEnabled
+ ? 'default'
+ : null;
+}
+
export function extractFastQuestion(
mentionStrippedText: string,
continuation = false,
diff --git a/apps/api/src/handlers/slack/events/message-entry.ts b/apps/api/src/handlers/slack/events/message-entry.ts
index b9e2848f2..c0b2930b2 100644
--- a/apps/api/src/handlers/slack/events/message-entry.ts
+++ b/apps/api/src/handlers/slack/events/message-entry.ts
@@ -55,8 +55,8 @@ import type { AutomatedSlackAppMentionEvent } from '../types.js';
import { processActiveRunMessage } from './active-run.js';
import {
isBareFastCommandInvocation,
- isFastCommandInvocation,
processFastAgentMessage,
+ resolveFastAgentEntryMode,
} from './fast-agent.js';
import { createFastAgentTaskLauncher } from './fast-agent-task-launcher.js';
import { processSnapshotResume } from './snapshot-resume.js';
@@ -1687,7 +1687,15 @@ async function handleSlackEntryEvent(params: {
activeTaskId: activeRun?.taskId,
});
- if (isFastCommandInvocation(event.text)) {
+ const fastAgentEntryMode = resolveFastAgentEntryMode({
+ text: event.text,
+ deploymentSettingEnabled: Env.R_SLACK_FAST_MODE_SETTING_ENABLED === true,
+ userDefaultEnabled:
+ userMapping.slackFastModeDefault &&
+ !isRemovedEvalCommandInvocation(event.text),
+ });
+
+ if (fastAgentEntryMode) {
startFastAgentResponse({
event,
slackInstallation,
@@ -1696,6 +1704,7 @@ async function handleSlackEntryEvent(params: {
userId: userMapping.userId,
teamId,
activeTaskId: activeRun?.taskId ?? null,
+ continuation: fastAgentEntryMode === 'default',
processingReactionName: ackEmoji,
errorLogPrefix: `❌ Background fast-agent response failed for thread ${threadId}:`,
});
diff --git a/apps/api/src/handlers/slack/helpers/user-mapping.test.ts b/apps/api/src/handlers/slack/helpers/user-mapping.test.ts
index b66f5fb43..af6021f79 100644
--- a/apps/api/src/handlers/slack/helpers/user-mapping.test.ts
+++ b/apps/api/src/handlers/slack/helpers/user-mapping.test.ts
@@ -24,6 +24,7 @@ vi.mock('@roomote/db/server', () => ({
users: {
id: 'users.id',
deletedAt: 'users.deletedAt',
+ metadata: 'users.metadata',
},
}));
@@ -51,6 +52,7 @@ describe('lookupSlackUserMapping', () => {
updatedAt,
matchedUserId: 'user-1',
userDeletedAt: null,
+ userMetadata: { slack_fast_mode_default: true },
},
]);
@@ -66,6 +68,7 @@ describe('lookupSlackUserMapping', () => {
userId: 'user-1',
createdAt,
updatedAt,
+ slackFastModeDefault: true,
},
hasInactiveMapping: false,
});
@@ -82,6 +85,7 @@ describe('lookupSlackUserMapping', () => {
updatedAt: new Date('2024-01-02T00:00:00.000Z'),
matchedUserId: 'user-1',
userDeletedAt: new Date('2024-02-01T00:00:00.000Z'),
+ userMetadata: {},
},
]);
diff --git a/apps/api/src/handlers/slack/helpers/user-mapping.ts b/apps/api/src/handlers/slack/helpers/user-mapping.ts
index 4aa738842..2ed0045f1 100644
--- a/apps/api/src/handlers/slack/helpers/user-mapping.ts
+++ b/apps/api/src/handlers/slack/helpers/user-mapping.ts
@@ -8,7 +8,7 @@ import {
} from '@roomote/db/server';
type SlackUserMappingLookup = {
- activeMapping: SlackUserMapping | null;
+ activeMapping: (SlackUserMapping & { slackFastModeDefault: boolean }) | null;
hasInactiveMapping: boolean;
};
@@ -26,6 +26,7 @@ export async function lookupSlackUserMapping(params: {
updatedAt: slackUserMappings.updatedAt,
matchedUserId: users.id,
userDeletedAt: users.deletedAt,
+ userMetadata: users.metadata,
})
.from(slackUserMappings)
.leftJoin(users, eq(users.id, slackUserMappings.userId))
@@ -59,6 +60,12 @@ export async function lookupSlackUserMapping(params: {
userId: row.userId,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
+ slackFastModeDefault:
+ typeof row.userMetadata === 'object' &&
+ row.userMetadata !== null &&
+ !Array.isArray(row.userMetadata) &&
+ (row.userMetadata as Record)
+ .slack_fast_mode_default === true,
},
hasInactiveMapping: false,
};
diff --git a/apps/docs/environment-variables.mdx b/apps/docs/environment-variables.mdx
index eadbe990d..e5a888b9b 100644
--- a/apps/docs/environment-variables.mdx
+++ b/apps/docs/environment-variables.mdx
@@ -127,6 +127,7 @@ as per-task auth tokens or workspace paths.
| `ROOMOTE_FORCE_TELEMETRY` | Development only | Force-enables telemetry in development or preview environments when a Ping endpoint is explicitly configured. |
| `R_CLOUD_ENABLED` | Roomote Cloud only | Deployment-managed switch for Roomote Cloud behavior, including required anonymous analytics and Cloud support integrations. Do not set this for self-hosted deployments. |
| `R_CURATED_INTEGRATIONS_DISABLED` | Optional | Operator policy for the curated **Settings > Integrations** catalog, which is enabled by default. Set to `true` and restart Roomote to prevent those integrations from being configured or used. Existing connections remain stored while disabled and become available again once the value is unset. Communications, source-control, inference, sandbox providers, and environment-defined MCP servers are unaffected. |
+| `R_SLACK_FAST_MODE_SETTING_ENABLED` | Optional | Set to `true` and restart Roomote to expose a personal setting that defaults a user's Slack messages to fast mode without requiring `!fast`. The setting is hidden and unavailable when this flag is unset. |
| `R_GITHUB_APP_SLUG` | GitHub setup | GitHub App slug used by server-rendered setup and GitHub integration flows. |
| `SETUP_TOKEN` | Required (non-local) | One-time bootstrap token that admits the first admin at `/setup`. Required on every non-local deployment — tokenless bootstrap is allowed only when `NODE_ENV` is not `production` and `R_APP_ENV` is `development`, so anything running with `NODE_ENV=production` needs it. Without it, first-admin bootstrap stays closed so nobody can claim the founding-admin slot by reaching the URL first. Optional only in local development. |
| `DASHBOARD_PASSWORD` | Production | Local fallback/admin password value used by the deployment. Generate a strong secret. |
diff --git a/apps/web/src/components/settings/UserPreferencesSection.test.tsx b/apps/web/src/components/settings/UserPreferencesSection.test.tsx
index ef268a3c7..cebbeedc4 100644
--- a/apps/web/src/components/settings/UserPreferencesSection.test.tsx
+++ b/apps/web/src/components/settings/UserPreferencesSection.test.tsx
@@ -3,28 +3,37 @@ import { fireEvent, render, screen, within } from '@testing-library/react';
type PersonalColorTheme = 'light' | 'dark' | 'system';
-const { colorThemeState, mindReaderModeState, narrationModeState } = vi.hoisted(
- () => ({
- colorThemeState: {
- colorTheme: 'system' as PersonalColorTheme,
- isLoading: false,
- isUpdating: false,
- setColorTheme: vi.fn(),
- },
- mindReaderModeState: {
- enabled: false,
- isLoading: false,
- isUpdating: false,
- setEnabled: vi.fn(),
- },
- narrationModeState: {
- enabled: false,
- isLoading: false,
- isUpdating: false,
- setEnabled: vi.fn(),
- },
- }),
-);
+const {
+ colorThemeState,
+ mindReaderModeState,
+ narrationModeState,
+ personalPreferencesState,
+} = vi.hoisted(() => ({
+ colorThemeState: {
+ colorTheme: 'system' as PersonalColorTheme,
+ isLoading: false,
+ isUpdating: false,
+ setColorTheme: vi.fn(),
+ },
+ mindReaderModeState: {
+ enabled: false,
+ isLoading: false,
+ isUpdating: false,
+ setEnabled: vi.fn(),
+ },
+ narrationModeState: {
+ enabled: false,
+ isLoading: false,
+ isUpdating: false,
+ setEnabled: vi.fn(),
+ },
+ personalPreferencesState: {
+ preferences: { slackFastModeDefault: false },
+ isLoading: false,
+ isUpdating: false,
+ setPreferences: vi.fn(),
+ },
+}));
vi.mock('@/hooks/useColorTheme', () => ({
useColorTheme: () => colorThemeState,
@@ -38,6 +47,10 @@ vi.mock('@/hooks/useMindReaderMode', () => ({
useMindReaderMode: () => mindReaderModeState,
}));
+vi.mock('@/hooks/usePersonalPreferences', () => ({
+ usePersonalPreferences: () => personalPreferencesState,
+}));
+
vi.mock('@/components/system', () => ({
Label: ({
children,
@@ -123,6 +136,9 @@ describe('UserPreferencesSection', () => {
narrationModeState.enabled = false;
narrationModeState.isLoading = false;
narrationModeState.isUpdating = false;
+ personalPreferencesState.preferences.slackFastModeDefault = false;
+ personalPreferencesState.isLoading = false;
+ personalPreferencesState.isUpdating = false;
});
it('renders user preference controls with the current state', () => {
@@ -204,4 +220,27 @@ describe('UserPreferencesSection', () => {
'system',
);
});
+
+ it('hides the Slack fast mode default when it is unavailable', () => {
+ render();
+
+ expect(
+ screen.queryByLabelText('Toggle Slack fast mode default'),
+ ).not.toBeInTheDocument();
+ });
+
+ it('updates the Slack fast mode default when it is available', () => {
+ personalPreferencesState.preferences.slackFastModeDefault = true;
+
+ render();
+
+ const toggle = screen.getByLabelText('Toggle Slack fast mode default');
+ expect(toggle).toBeChecked();
+
+ fireEvent.click(toggle);
+
+ expect(personalPreferencesState.setPreferences).toHaveBeenCalledWith({
+ slackFastModeDefault: false,
+ });
+ });
});
diff --git a/apps/web/src/components/settings/UserPreferencesSection.tsx b/apps/web/src/components/settings/UserPreferencesSection.tsx
index 1f54c82fb..927b4089d 100644
--- a/apps/web/src/components/settings/UserPreferencesSection.tsx
+++ b/apps/web/src/components/settings/UserPreferencesSection.tsx
@@ -3,6 +3,7 @@
import { useColorTheme } from '@/hooks/useColorTheme';
import { useMindReaderMode } from '@/hooks/useMindReaderMode';
import { useNarrationMode } from '@/hooks/useNarrationMode';
+import { usePersonalPreferences } from '@/hooks/usePersonalPreferences';
import type { PersonalColorTheme } from '@/types/preferences';
import {
@@ -27,7 +28,11 @@ const COLOR_THEME_OPTIONS: ReadonlyArray<{
{ label: 'Auto', value: 'system' },
];
-export function UserPreferencesSection() {
+export function UserPreferencesSection({
+ slackFastModeDefaultAvailable = false,
+}: {
+ slackFastModeDefaultAvailable?: boolean;
+}) {
const {
colorTheme,
isLoading: isThemeLoading,
@@ -46,6 +51,15 @@ export function UserPreferencesSection() {
isUpdating: isNarrationModeUpdating,
setEnabled: setNarrationModeEnabled,
} = useNarrationMode();
+ const {
+ preferences,
+ isLoading: isSlackFastModeDefaultLoading,
+ isUpdating: isSlackFastModeDefaultUpdating,
+ setPreferences,
+ } = usePersonalPreferences({
+ enabled: slackFastModeDefaultAvailable,
+ errorMessage: 'Failed to update the Slack fast mode default.',
+ });
const isThemeDisabled = isThemeLoading || isThemeUpdating;
return (
@@ -113,6 +127,29 @@ export function UserPreferencesSection() {
+
+ {slackFastModeDefaultAvailable ? (
+
+
+ setPreferences({ slackFastModeDefault: enabled })
+ }
+ />
+
+
+ Default Slack messages to fast mode
+
+
+ Use fast mode for your Slack messages without adding !fast.
+
+
+
+ ) : null}
);
diff --git a/apps/web/src/components/settings/pages/PersonalSettingsPage.test.tsx b/apps/web/src/components/settings/pages/PersonalSettingsPage.test.tsx
index d7bb3aa31..bfdf2b04f 100644
--- a/apps/web/src/components/settings/pages/PersonalSettingsPage.test.tsx
+++ b/apps/web/src/components/settings/pages/PersonalSettingsPage.test.tsx
@@ -53,6 +53,7 @@ describe('PersonalSettingsPage', () => {
,
);
@@ -70,6 +71,7 @@ describe('PersonalSettingsPage', () => {
,
);
diff --git a/apps/web/src/components/settings/pages/PersonalSettingsPage.tsx b/apps/web/src/components/settings/pages/PersonalSettingsPage.tsx
index a7dbe23f4..d3f25e00e 100644
--- a/apps/web/src/components/settings/pages/PersonalSettingsPage.tsx
+++ b/apps/web/src/components/settings/pages/PersonalSettingsPage.tsx
@@ -13,10 +13,12 @@ export function PersonalSettingsPage({
profile,
canChangePassword,
canSetPassword,
+ slackFastModeDefaultAvailable,
}: {
profile: UserProfileSectionProfile;
canChangePassword: boolean;
canSetPassword: boolean;
+ slackFastModeDefaultAvailable: boolean;
}) {
return (
@@ -27,7 +29,9 @@ export function PersonalSettingsPage({
{canChangePassword || canSetPassword ? (
) : null}
-
+
);
diff --git a/apps/web/src/components/settings/pages/PersonalSettingsRoute.test.tsx b/apps/web/src/components/settings/pages/PersonalSettingsRoute.test.tsx
index 28dbad673..6ae61ac89 100644
--- a/apps/web/src/components/settings/pages/PersonalSettingsRoute.test.tsx
+++ b/apps/web/src/components/settings/pages/PersonalSettingsRoute.test.tsx
@@ -1,10 +1,15 @@
import { render } from '@testing-library/react';
let accountCapabilities:
- | { canChangePassword: boolean; canSetPassword: boolean }
+ | {
+ canChangePassword: boolean;
+ canSetPassword: boolean;
+ slackFastModeDefaultAvailable: boolean;
+ }
| undefined = {
canChangePassword: true,
canSetPassword: false,
+ slackFastModeDefaultAvailable: true,
};
const { personalSettingsPageMock } = vi.hoisted(() => ({
@@ -41,7 +46,11 @@ import { PersonalSettingsRoute } from './PersonalSettingsRoute';
describe('PersonalSettingsRoute', () => {
beforeEach(() => {
- accountCapabilities = { canChangePassword: true, canSetPassword: false };
+ accountCapabilities = {
+ canChangePassword: true,
+ canSetPassword: false,
+ slackFastModeDefaultAvailable: true,
+ };
personalSettingsPageMock.mockClear();
});
@@ -52,6 +61,7 @@ describe('PersonalSettingsRoute', () => {
{
canChangePassword: true,
canSetPassword: false,
+ slackFastModeDefaultAvailable: true,
profile: {
email: 'ada@example.com',
imageUrl: 'https://example.com/ada.png',
diff --git a/apps/web/src/components/settings/pages/PersonalSettingsRoute.tsx b/apps/web/src/components/settings/pages/PersonalSettingsRoute.tsx
index 58487d136..f89cfcf41 100644
--- a/apps/web/src/components/settings/pages/PersonalSettingsRoute.tsx
+++ b/apps/web/src/components/settings/pages/PersonalSettingsRoute.tsx
@@ -18,6 +18,9 @@ export function PersonalSettingsRoute() {
{
colorTheme: 'system',
mindReaderMode: false,
narrationMode: false,
+ slackFastModeDefault: false,
});
});
diff --git a/apps/web/src/hooks/usePersonalPreferences.ts b/apps/web/src/hooks/usePersonalPreferences.ts
index 32a812ee1..c85ad891f 100644
--- a/apps/web/src/hooks/usePersonalPreferences.ts
+++ b/apps/web/src/hooks/usePersonalPreferences.ts
@@ -51,6 +51,10 @@ function mergeResultForUpdatedFields(
updates.narrationMode === undefined
? mergedPreferences.narrationMode
: result.narrationMode,
+ slackFastModeDefault:
+ updates.slackFastModeDefault === undefined
+ ? mergedPreferences.slackFastModeDefault
+ : result.slackFastModeDefault,
};
}
@@ -81,6 +85,12 @@ function rollbackUpdatedFields(
mergedPreferences.narrationMode === optimisticPreferences.narrationMode
? previousPreferences.narrationMode
: mergedPreferences.narrationMode,
+ slackFastModeDefault:
+ updates.slackFastModeDefault !== undefined &&
+ mergedPreferences.slackFastModeDefault ===
+ optimisticPreferences.slackFastModeDefault
+ ? previousPreferences.slackFastModeDefault
+ : mergedPreferences.slackFastModeDefault,
};
}
diff --git a/apps/web/src/trpc/commands/preferences/index.ts b/apps/web/src/trpc/commands/preferences/index.ts
index 226187af2..5e1825941 100644
--- a/apps/web/src/trpc/commands/preferences/index.ts
+++ b/apps/web/src/trpc/commands/preferences/index.ts
@@ -3,6 +3,7 @@ import { headers } from 'next/headers';
import type { UserAuthSuccess } from '@/types';
import { getAuth } from '@/lib/server/auth';
+import { Env } from '@/lib/server/env';
import { userHasCredentialAccount } from '@/lib/server/user-management';
import {
DEFAULT_PERSONAL_PREFERENCES,
@@ -36,6 +37,9 @@ function normalizePersonalPreferences(
typeof metadata.narration_mode === 'boolean'
? metadata.narration_mode
: DEFAULT_PERSONAL_PREFERENCES.narrationMode,
+ slackFastModeDefault:
+ Env.R_SLACK_FAST_MODE_SETTING_ENABLED === true &&
+ metadata.slack_fast_mode_default === true,
};
}
@@ -62,6 +66,8 @@ export async function getPersonalAccountCapabilitiesCommand(
return {
canChangePassword: hasCredentialAccount,
canSetPassword: !hasCredentialAccount,
+ slackFastModeDefaultAvailable:
+ Env.R_SLACK_FAST_MODE_SETTING_ENABLED === true,
};
}
@@ -113,6 +119,15 @@ export async function updatePersonalPreferencesCommand(
): Promise {
const nextMetadataRecord: UserMetadataRecord = {};
+ if (
+ input.slackFastModeDefault !== undefined &&
+ Env.R_SLACK_FAST_MODE_SETTING_ENABLED !== true
+ ) {
+ throw new Error(
+ 'The Slack fast mode default setting is not enabled for this deployment.',
+ );
+ }
+
if (input.colorTheme !== undefined) {
nextMetadataRecord.color_theme = input.colorTheme;
}
@@ -125,6 +140,10 @@ export async function updatePersonalPreferencesCommand(
nextMetadataRecord.narration_mode = input.narrationMode;
}
+ if (input.slackFastModeDefault !== undefined) {
+ nextMetadataRecord.slack_fast_mode_default = input.slackFastModeDefault;
+ }
+
if (Object.keys(nextMetadataRecord).length === 0) {
return getPersonalPreferencesCommand(auth);
}
diff --git a/apps/web/src/trpc/commands/preferences/personal-preferences.test.ts b/apps/web/src/trpc/commands/preferences/personal-preferences.test.ts
index ce37848b8..1cdbcf6ef 100644
--- a/apps/web/src/trpc/commands/preferences/personal-preferences.test.ts
+++ b/apps/web/src/trpc/commands/preferences/personal-preferences.test.ts
@@ -2,6 +2,12 @@ import { db, eq, userFactory, users } from '@roomote/db/server';
import type { UserAuthSuccess } from '@/types';
+const { mockEnv } = vi.hoisted(() => ({
+ mockEnv: { R_SLACK_FAST_MODE_SETTING_ENABLED: false },
+}));
+
+vi.mock('@/lib/server/env', () => ({ Env: mockEnv }));
+
import {
getPersonalPreferencesCommand,
updatePersonalPreferencesCommand,
@@ -12,6 +18,10 @@ function buildAuth(userId: string) {
}
describe('personal preferences', () => {
+ beforeEach(() => {
+ mockEnv.R_SLACK_FAST_MODE_SETTING_ENABLED = false;
+ });
+
it('defaults mind reader mode to disabled', async () => {
const user = await userFactory.create();
@@ -60,4 +70,48 @@ describe('personal preferences', () => {
}),
);
});
+
+ it('rejects Slack fast mode updates when the deployment setting is disabled', async () => {
+ const user = await userFactory.create();
+
+ await expect(
+ updatePersonalPreferencesCommand(buildAuth(user.id), {
+ slackFastModeDefault: true,
+ }),
+ ).rejects.toThrow(
+ 'The Slack fast mode default setting is not enabled for this deployment.',
+ );
+ });
+
+ it('does not expose a stored Slack fast mode default when the deployment setting is disabled', async () => {
+ const user = await userFactory.create({
+ metadata: { slack_fast_mode_default: true },
+ });
+
+ await expect(
+ getPersonalPreferencesCommand(buildAuth(user.id)),
+ ).resolves.toEqual(
+ expect.objectContaining({ slackFastModeDefault: false }),
+ );
+ });
+
+ it('persists the Slack fast mode default when the deployment setting is enabled', async () => {
+ mockEnv.R_SLACK_FAST_MODE_SETTING_ENABLED = true;
+ const user = await userFactory.create();
+
+ await expect(
+ updatePersonalPreferencesCommand(buildAuth(user.id), {
+ slackFastModeDefault: true,
+ }),
+ ).resolves.toEqual(expect.objectContaining({ slackFastModeDefault: true }));
+
+ const storedUser = await db.query.users.findFirst({
+ where: eq(users.id, user.id),
+ columns: { metadata: true },
+ });
+
+ expect(storedUser?.metadata).toEqual(
+ expect.objectContaining({ slack_fast_mode_default: true }),
+ );
+ });
});
diff --git a/apps/web/src/trpc/routers/_app.ts b/apps/web/src/trpc/routers/_app.ts
index 39a09010a..eda088036 100644
--- a/apps/web/src/trpc/routers/_app.ts
+++ b/apps/web/src/trpc/routers/_app.ts
@@ -1432,12 +1432,14 @@ export const appRouter = createRouter({
colorTheme: z.enum(PERSONAL_COLOR_THEMES).optional(),
mindReaderMode: z.boolean().optional(),
narrationMode: z.boolean().optional(),
+ slackFastModeDefault: z.boolean().optional(),
})
.refine(
(input) =>
input.colorTheme !== undefined ||
input.mindReaderMode !== undefined ||
- input.narrationMode !== undefined,
+ input.narrationMode !== undefined ||
+ input.slackFastModeDefault !== undefined,
{
message: 'Expected at least one personal preference to update.',
},
diff --git a/apps/web/src/types/preferences.ts b/apps/web/src/types/preferences.ts
index dbcf508e0..e0d1db788 100644
--- a/apps/web/src/types/preferences.ts
+++ b/apps/web/src/types/preferences.ts
@@ -16,6 +16,7 @@ export interface PersonalPreferences {
colorTheme: PersonalColorTheme;
mindReaderMode: boolean;
narrationMode: boolean;
+ slackFastModeDefault: boolean;
}
export type PersonalPreferencesUpdate = Partial;
@@ -24,4 +25,5 @@ export const DEFAULT_PERSONAL_PREFERENCES: PersonalPreferences = {
colorTheme: 'system',
mindReaderMode: false,
narrationMode: false,
+ slackFastModeDefault: false,
};
diff --git a/docker-compose.production.yml b/docker-compose.production.yml
index bdf31ef35..8aacfe159 100644
--- a/docker-compose.production.yml
+++ b/docker-compose.production.yml
@@ -23,6 +23,7 @@ x-roomote-production-env: &roomote-production-env
R_PUBLIC_URL: https://${ROOMOTE_APP_DOMAIN:?ROOMOTE_APP_DOMAIN is required}
R_INSTANCE_ID: ${R_INSTANCE_ID:-}
R_CURATED_INTEGRATIONS_DISABLED: ${R_CURATED_INTEGRATIONS_DISABLED:-false}
+ R_SLACK_FAST_MODE_SETTING_ENABLED: ${R_SLACK_FAST_MODE_SETTING_ENABLED:-false}
TRPC_URL: http://api:3001
PREVIEW_PROXY_BASE_URL: https://${ROOMOTE_PREVIEW_DOMAIN:?ROOMOTE_PREVIEW_DOMAIN is required}
NEXT_PUBLIC_PREVIEW_PROXY_BASE_URL: https://${ROOMOTE_PREVIEW_DOMAIN:?ROOMOTE_PREVIEW_DOMAIN is required}
diff --git a/docker-compose.self-host.yml b/docker-compose.self-host.yml
index 59691603d..b507f98fe 100644
--- a/docker-compose.self-host.yml
+++ b/docker-compose.self-host.yml
@@ -22,6 +22,7 @@ x-roomote-env: &roomote-env
R_APP_URL: ${R_PUBLIC_URL:-http://localhost:13000}
R_INSTANCE_ID: ${R_INSTANCE_ID:-}
R_CURATED_INTEGRATIONS_DISABLED: ${R_CURATED_INTEGRATIONS_DISABLED:-false}
+ R_SLACK_FAST_MODE_SETTING_ENABLED: ${R_SLACK_FAST_MODE_SETTING_ENABLED:-false}
# The Brain: supplying this key gives the deployment shared memory that
# agents consult. It powers the brain service's embeddings and is the
# single activation signal — there is no Settings UI for it.
diff --git a/packages/env/src/__tests__/index.test.ts b/packages/env/src/__tests__/index.test.ts
index 1a36413f4..a72d0097a 100644
--- a/packages/env/src/__tests__/index.test.ts
+++ b/packages/env/src/__tests__/index.test.ts
@@ -226,6 +226,22 @@ describe('Env', () => {
expect(areCuratedIntegrationsDisabled('0')).toBe(false);
});
+ it('keeps the Slack fast mode setting opt-in', () => {
+ const runtimeEnv = { ...process.env };
+ delete runtimeEnv.SKIP_ENV_VALIDATION;
+ delete runtimeEnv.R_SLACK_FAST_MODE_SETTING_ENABLED;
+
+ expect(createRoomoteEnv(runtimeEnv).R_SLACK_FAST_MODE_SETTING_ENABLED).toBe(
+ false,
+ );
+ expect(
+ createRoomoteEnv({
+ ...runtimeEnv,
+ R_SLACK_FAST_MODE_SETTING_ENABLED: 'true',
+ }).R_SLACK_FAST_MODE_SETTING_ENABLED,
+ ).toBe(true);
+ });
+
it('accepts valid Ping instance IDs and rejects invalid ones', () => {
const runtimeEnv = { ...process.env };
delete runtimeEnv.SKIP_ENV_VALIDATION;
diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts
index 824bd3c8a..3bf0e2db4 100644
--- a/packages/env/src/index.ts
+++ b/packages/env/src/index.ts
@@ -124,6 +124,8 @@ const serverSchema = {
// Roomote Cloud-only analytics and support integrations. These values are
// intentionally not used by self-hosted deployments.
R_CLOUD_ENABLED: optInBoolean(),
+ // Exposes the per-user setting that defaults Slack messages to fast mode.
+ R_SLACK_FAST_MODE_SETTING_ENABLED: optInBoolean(),
// Operator policy for the curated Settings > Integrations catalog. Enabled
// by default; operators opt out explicitly. Existing connections remain
// stored but cannot be configured or used while disabled.
From 596c13e803c7c247c09b79e3745e1555c6dcd1e9 Mon Sep 17 00:00:00 2001
From: "@mrubens" <2600+mrubens@users.noreply.github.com>
Date: Sun, 16 Aug 2026 21:51:02 +0000
Subject: [PATCH 2/4] chore: keep fast entry mode internal
---
apps/api/src/handlers/slack/events/fast-agent.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/apps/api/src/handlers/slack/events/fast-agent.ts b/apps/api/src/handlers/slack/events/fast-agent.ts
index 04f49efc4..1cb067bfa 100644
--- a/apps/api/src/handlers/slack/events/fast-agent.ts
+++ b/apps/api/src/handlers/slack/events/fast-agent.ts
@@ -27,7 +27,7 @@ export function isBareFastCommandInvocation(text: string): boolean {
return /^!fast(?:\s|$)/i.test(text.trimStart());
}
-export type FastAgentEntryMode = 'explicit' | 'default';
+type FastAgentEntryMode = 'explicit' | 'default';
export function resolveFastAgentEntryMode(params: {
text: string;
From f809b936647e6e658166d9d71da07ad1bb3bf885 Mon Sep 17 00:00:00 2001
From: "@mrubens" <2600+mrubens@users.noreply.github.com>
Date: Sun, 16 Aug 2026 22:16:18 +0000
Subject: [PATCH 3/4] fix: honor Slack fast default in auto-start channels
---
.../slack/__tests__/fast-agent.test.ts | 8 +-
.../channel-auto-start-unlinked.test.ts | 77 ++++++++++++++++++-
.../src/handlers/slack/events/fast-agent.ts | 4 +-
.../handlers/slack/events/message-entry.ts | 24 ++++--
.../trpc/commands/preferences/index.test.ts | 2 +
5 files changed, 100 insertions(+), 15 deletions(-)
diff --git a/apps/api/src/handlers/slack/__tests__/fast-agent.test.ts b/apps/api/src/handlers/slack/__tests__/fast-agent.test.ts
index ce1e7f55d..c31141c74 100644
--- a/apps/api/src/handlers/slack/__tests__/fast-agent.test.ts
+++ b/apps/api/src/handlers/slack/__tests__/fast-agent.test.ts
@@ -31,7 +31,7 @@ describe('Slack fast-agent helpers', () => {
it('keeps ordinary messages on standard routing when the deployment flag is disabled', () => {
expect(
resolveFastAgentEntryMode({
- text: 'please fix this',
+ explicitInvocation: false,
deploymentSettingEnabled: false,
userDefaultEnabled: true,
}),
@@ -41,7 +41,7 @@ describe('Slack fast-agent helpers', () => {
it('keeps ordinary messages on standard routing when the user setting is off', () => {
expect(
resolveFastAgentEntryMode({
- text: 'please fix this',
+ explicitInvocation: false,
deploymentSettingEnabled: true,
userDefaultEnabled: false,
}),
@@ -51,7 +51,7 @@ describe('Slack fast-agent helpers', () => {
it('defaults ordinary messages to fast mode when both settings are enabled', () => {
expect(
resolveFastAgentEntryMode({
- text: 'please fix this',
+ explicitInvocation: false,
deploymentSettingEnabled: true,
userDefaultEnabled: true,
}),
@@ -61,7 +61,7 @@ describe('Slack fast-agent helpers', () => {
it('preserves explicit !fast routing regardless of the user setting', () => {
expect(
resolveFastAgentEntryMode({
- text: '!fast please fix this',
+ explicitInvocation: true,
deploymentSettingEnabled: true,
userDefaultEnabled: true,
}),
diff --git a/apps/api/src/handlers/slack/events/channel-auto-start-unlinked.test.ts b/apps/api/src/handlers/slack/events/channel-auto-start-unlinked.test.ts
index 4b72eb055..aecb1430f 100644
--- a/apps/api/src/handlers/slack/events/channel-auto-start-unlinked.test.ts
+++ b/apps/api/src/handlers/slack/events/channel-auto-start-unlinked.test.ts
@@ -1,7 +1,15 @@
import { describe, expect, it, vi, beforeEach } from 'vitest';
-const { showConnectAccountMock } = vi.hoisted(() => ({
+const {
+ fastAgentMessageMock,
+ showConnectAccountMock,
+ startTaskMock,
+ userMappingRowsMock,
+} = vi.hoisted(() => ({
+ fastAgentMessageMock: vi.fn(),
showConnectAccountMock: vi.fn(),
+ startTaskMock: vi.fn(),
+ userMappingRowsMock: vi.fn(),
}));
const { enrichSlackMessageEventMock, isRoomoteAuthoredSlackEventMock } =
@@ -11,7 +19,11 @@ const { enrichSlackMessageEventMock, isRoomoteAuthoredSlackEventMock } =
}));
vi.mock('@roomote/env', () => ({
- Env: { TRPC_URL: null, R_APP_URL: 'http://localhost:3000' },
+ Env: {
+ TRPC_URL: null,
+ R_APP_URL: 'http://localhost:3000',
+ R_SLACK_FAST_MODE_SETTING_ENABLED: true,
+ },
}));
vi.mock('@roomote/cloud-agents/server', () => ({
@@ -31,7 +43,17 @@ vi.mock('../helpers/event-normalization.js', () => ({
vi.mock('@roomote/slack', async (importOriginal) => ({
...(await importOriginal()),
+ resolveSlackReactionNames: vi.fn().mockResolvedValue({
+ ackEmoji: 'eyes',
+ completionEmoji: 'white_check_mark',
+ }),
showConnectAccount: showConnectAccountMock,
+ startAutoRoutedSlackTask: startTaskMock,
+}));
+
+vi.mock('./fast-agent.js', async (importOriginal) => ({
+ ...(await importOriginal()),
+ processFastAgentMessage: fastAgentMessageMock,
}));
const redisClientMock = {
@@ -60,7 +82,7 @@ const userMappingSelectChain = () => ({
from: vi.fn(() => ({
leftJoin: vi.fn(() => ({
where: vi.fn(() => ({
- limit: vi.fn().mockResolvedValue([]),
+ limit: userMappingRowsMock,
})),
})),
})),
@@ -78,6 +100,8 @@ describe('channel auto-start unlinked author', () => {
beforeEach(() => {
vi.clearAllMocks();
showConnectAccountMock.mockResolvedValue(undefined);
+ fastAgentMessageMock.mockResolvedValue(undefined);
+ userMappingRowsMock.mockResolvedValue([]);
});
it('prompts an unlinked human author to connect their account instead of silently skipping', async () => {
@@ -114,4 +138,51 @@ describe('channel auto-start unlinked author', () => {
expect.anything(),
);
}, 30000);
+
+ it('routes an opted-in linked author to fast mode before channel auto-start', async () => {
+ userMappingRowsMock.mockResolvedValue([
+ {
+ id: 'mapping-1',
+ slackUserId: 'U456',
+ slackTeamId: 'T123',
+ userId: 'user-1',
+ createdAt: new Date('2026-01-01T00:00:00.000Z'),
+ updatedAt: new Date('2026-01-01T00:00:00.000Z'),
+ matchedUserId: 'user-1',
+ userDeletedAt: null,
+ userMetadata: { slack_fast_mode_default: true },
+ },
+ ]);
+ const { handleMessageOrAppMentionEvent } =
+ await import('./message-entry.js');
+
+ await handleMessageOrAppMentionEvent({
+ event: {
+ type: 'message',
+ channel: 'C123',
+ user: 'U456',
+ text: 'please look into this',
+ ts: '112.000',
+ channel_type: 'channel',
+ } as never,
+ context: {
+ slackInstallation: { teamId: 'T123', botUserId: 'UBOT' } as never,
+ slack: {} as never,
+ teamId: 'T123',
+ } as never,
+ });
+
+ expect(fastAgentMessageMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ continuation: true,
+ event: expect.objectContaining({
+ text: 'please look into this',
+ user: 'U456',
+ }),
+ teamId: 'T123',
+ userId: 'user-1',
+ }),
+ );
+ expect(startTaskMock).not.toHaveBeenCalled();
+ }, 30000);
});
diff --git a/apps/api/src/handlers/slack/events/fast-agent.ts b/apps/api/src/handlers/slack/events/fast-agent.ts
index 1cb067bfa..8e9c88ff0 100644
--- a/apps/api/src/handlers/slack/events/fast-agent.ts
+++ b/apps/api/src/handlers/slack/events/fast-agent.ts
@@ -30,11 +30,11 @@ export function isBareFastCommandInvocation(text: string): boolean {
type FastAgentEntryMode = 'explicit' | 'default';
export function resolveFastAgentEntryMode(params: {
- text: string;
+ explicitInvocation: boolean;
deploymentSettingEnabled: boolean;
userDefaultEnabled: boolean;
}): FastAgentEntryMode | null {
- if (isFastCommandInvocation(params.text)) {
+ if (params.explicitInvocation) {
return 'explicit';
}
diff --git a/apps/api/src/handlers/slack/events/message-entry.ts b/apps/api/src/handlers/slack/events/message-entry.ts
index c0b2930b2..4daa20016 100644
--- a/apps/api/src/handlers/slack/events/message-entry.ts
+++ b/apps/api/src/handlers/slack/events/message-entry.ts
@@ -55,6 +55,7 @@ import type { AutomatedSlackAppMentionEvent } from '../types.js';
import { processActiveRunMessage } from './active-run.js';
import {
isBareFastCommandInvocation,
+ isFastCommandInvocation,
processFastAgentMessage,
resolveFastAgentEntryMode,
} from './fast-agent.js';
@@ -1223,11 +1224,21 @@ async function maybeHandleChannelAutoStart(params: {
const { ackEmoji } = await resolveSlackReactionNames();
- if (
- userMapping &&
- typeof channelAutoStartEvent.user === 'string' &&
- isBareFastCommandInvocation(channelAutoStartEvent.text)
- ) {
+ const fastAgentEntryMode =
+ userMapping && typeof channelAutoStartEvent.user === 'string'
+ ? resolveFastAgentEntryMode({
+ explicitInvocation: isBareFastCommandInvocation(
+ channelAutoStartEvent.text,
+ ),
+ deploymentSettingEnabled:
+ Env.R_SLACK_FAST_MODE_SETTING_ENABLED === true,
+ userDefaultEnabled:
+ userMapping.slackFastModeDefault &&
+ !isRemovedEvalCommandInvocation(channelAutoStartEvent.text),
+ })
+ : null;
+
+ if (fastAgentEntryMode && userMapping) {
startFastAgentResponse({
event: { ...channelAutoStartEvent, user: channelAutoStartEvent.user },
slackInstallation: context.slackInstallation,
@@ -1236,6 +1247,7 @@ async function maybeHandleChannelAutoStart(params: {
userId: userMapping.userId,
teamId: context.teamId,
usageText: 'Use `!fast ` in this channel.',
+ continuation: fastAgentEntryMode === 'default',
processingReactionName: ackEmoji,
errorLogPrefix: `❌ Background fast-agent response failed for auto-start thread ${channelAutoStartEvent.ts}:`,
});
@@ -1688,7 +1700,7 @@ async function handleSlackEntryEvent(params: {
});
const fastAgentEntryMode = resolveFastAgentEntryMode({
- text: event.text,
+ explicitInvocation: isFastCommandInvocation(event.text),
deploymentSettingEnabled: Env.R_SLACK_FAST_MODE_SETTING_ENABLED === true,
userDefaultEnabled:
userMapping.slackFastModeDefault &&
diff --git a/apps/web/src/trpc/commands/preferences/index.test.ts b/apps/web/src/trpc/commands/preferences/index.test.ts
index 20c2973af..c36610270 100644
--- a/apps/web/src/trpc/commands/preferences/index.test.ts
+++ b/apps/web/src/trpc/commands/preferences/index.test.ts
@@ -41,6 +41,7 @@ describe('personal account capabilities', () => {
await expect(getPersonalAccountCapabilitiesCommand(auth)).resolves.toEqual({
canChangePassword: false,
canSetPassword: true,
+ slackFastModeDefaultAvailable: false,
});
});
@@ -50,6 +51,7 @@ describe('personal account capabilities', () => {
await expect(getPersonalAccountCapabilitiesCommand(auth)).resolves.toEqual({
canChangePassword: true,
canSetPassword: false,
+ slackFastModeDefaultAvailable: false,
});
});
From 11af0785519cd7dc9a1c1420add2ad984edbbbbd Mon Sep 17 00:00:00 2001
From: "@mrubens" <2600+mrubens@users.noreply.github.com>
Date: Sun, 16 Aug 2026 23:18:08 +0000
Subject: [PATCH 4/4] refactor: generalize communications fast default naming
---
.env.production.example | 5 ++--
.../channel-auto-start-unlinked.test.ts | 4 +--
.../handlers/slack/events/message-entry.ts | 9 +++---
.../slack/helpers/user-mapping.test.ts | 4 +--
.../handlers/slack/helpers/user-mapping.ts | 8 ++++--
apps/docs/environment-variables.mdx | 2 +-
.../settings/UserPreferencesSection.test.tsx | 22 +++++++++------
.../settings/UserPreferencesSection.tsx | 27 +++++++++---------
.../pages/PersonalSettingsPage.test.tsx | 4 +--
.../settings/pages/PersonalSettingsPage.tsx | 8 ++++--
.../pages/PersonalSettingsRoute.test.tsx | 8 +++---
.../settings/pages/PersonalSettingsRoute.tsx | 5 ++--
.../usePersonalPreferences.client.test.tsx | 2 +-
apps/web/src/hooks/usePersonalPreferences.ts | 20 ++++++-------
.../trpc/commands/preferences/index.test.ts | 4 +--
.../src/trpc/commands/preferences/index.ts | 21 +++++++-------
.../preferences/personal-preferences.test.ts | 28 ++++++++++---------
apps/web/src/trpc/routers/_app.ts | 4 +--
apps/web/src/types/preferences.ts | 4 +--
docker-compose.production.yml | 2 +-
docker-compose.self-host.yml | 2 +-
packages/env/src/__tests__/index.test.ts | 14 +++++-----
packages/env/src/index.ts | 4 +--
23 files changed, 113 insertions(+), 98 deletions(-)
diff --git a/.env.production.example b/.env.production.example
index 934c9fcd5..0c271c189 100644
--- a/.env.production.example
+++ b/.env.production.example
@@ -154,8 +154,9 @@ DEFAULT_COMPUTE_PROVIDER=docker
# Set to true to prevent curated integrations from being configured or used.
# Existing connections remain stored and become available again once unset.
# R_CURATED_INTEGRATIONS_DISABLED=true
-# Set to true to let users default their Slack messages to fast mode.
-# R_SLACK_FAST_MODE_SETTING_ENABLED=true
+# Set to true to let users default supported communications messages to fast mode.
+# Currently applies to Slack messages only.
+# R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED=true
# SLACK_APP_ID=
# R_SLACK_SIGNING_SECRET=
# R_TELEGRAM_BOT_TOKEN=
diff --git a/apps/api/src/handlers/slack/events/channel-auto-start-unlinked.test.ts b/apps/api/src/handlers/slack/events/channel-auto-start-unlinked.test.ts
index aecb1430f..d823fdcb0 100644
--- a/apps/api/src/handlers/slack/events/channel-auto-start-unlinked.test.ts
+++ b/apps/api/src/handlers/slack/events/channel-auto-start-unlinked.test.ts
@@ -22,7 +22,7 @@ vi.mock('@roomote/env', () => ({
Env: {
TRPC_URL: null,
R_APP_URL: 'http://localhost:3000',
- R_SLACK_FAST_MODE_SETTING_ENABLED: true,
+ R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED: true,
},
}));
@@ -150,7 +150,7 @@ describe('channel auto-start unlinked author', () => {
updatedAt: new Date('2026-01-01T00:00:00.000Z'),
matchedUserId: 'user-1',
userDeletedAt: null,
- userMetadata: { slack_fast_mode_default: true },
+ userMetadata: { communications_fast_mode_default: true },
},
]);
const { handleMessageOrAppMentionEvent } =
diff --git a/apps/api/src/handlers/slack/events/message-entry.ts b/apps/api/src/handlers/slack/events/message-entry.ts
index 4daa20016..2308f4827 100644
--- a/apps/api/src/handlers/slack/events/message-entry.ts
+++ b/apps/api/src/handlers/slack/events/message-entry.ts
@@ -1231,9 +1231,9 @@ async function maybeHandleChannelAutoStart(params: {
channelAutoStartEvent.text,
),
deploymentSettingEnabled:
- Env.R_SLACK_FAST_MODE_SETTING_ENABLED === true,
+ Env.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED === true,
userDefaultEnabled:
- userMapping.slackFastModeDefault &&
+ userMapping.communicationsFastModeDefault &&
!isRemovedEvalCommandInvocation(channelAutoStartEvent.text),
})
: null;
@@ -1701,9 +1701,10 @@ async function handleSlackEntryEvent(params: {
const fastAgentEntryMode = resolveFastAgentEntryMode({
explicitInvocation: isFastCommandInvocation(event.text),
- deploymentSettingEnabled: Env.R_SLACK_FAST_MODE_SETTING_ENABLED === true,
+ deploymentSettingEnabled:
+ Env.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED === true,
userDefaultEnabled:
- userMapping.slackFastModeDefault &&
+ userMapping.communicationsFastModeDefault &&
!isRemovedEvalCommandInvocation(event.text),
});
diff --git a/apps/api/src/handlers/slack/helpers/user-mapping.test.ts b/apps/api/src/handlers/slack/helpers/user-mapping.test.ts
index af6021f79..f339fbb5b 100644
--- a/apps/api/src/handlers/slack/helpers/user-mapping.test.ts
+++ b/apps/api/src/handlers/slack/helpers/user-mapping.test.ts
@@ -52,7 +52,7 @@ describe('lookupSlackUserMapping', () => {
updatedAt,
matchedUserId: 'user-1',
userDeletedAt: null,
- userMetadata: { slack_fast_mode_default: true },
+ userMetadata: { communications_fast_mode_default: true },
},
]);
@@ -68,7 +68,7 @@ describe('lookupSlackUserMapping', () => {
userId: 'user-1',
createdAt,
updatedAt,
- slackFastModeDefault: true,
+ communicationsFastModeDefault: true,
},
hasInactiveMapping: false,
});
diff --git a/apps/api/src/handlers/slack/helpers/user-mapping.ts b/apps/api/src/handlers/slack/helpers/user-mapping.ts
index 2ed0045f1..5537804bc 100644
--- a/apps/api/src/handlers/slack/helpers/user-mapping.ts
+++ b/apps/api/src/handlers/slack/helpers/user-mapping.ts
@@ -8,7 +8,9 @@ import {
} from '@roomote/db/server';
type SlackUserMappingLookup = {
- activeMapping: (SlackUserMapping & { slackFastModeDefault: boolean }) | null;
+ activeMapping:
+ | (SlackUserMapping & { communicationsFastModeDefault: boolean })
+ | null;
hasInactiveMapping: boolean;
};
@@ -60,12 +62,12 @@ export async function lookupSlackUserMapping(params: {
userId: row.userId,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
- slackFastModeDefault:
+ communicationsFastModeDefault:
typeof row.userMetadata === 'object' &&
row.userMetadata !== null &&
!Array.isArray(row.userMetadata) &&
(row.userMetadata as Record)
- .slack_fast_mode_default === true,
+ .communications_fast_mode_default === true,
},
hasInactiveMapping: false,
};
diff --git a/apps/docs/environment-variables.mdx b/apps/docs/environment-variables.mdx
index e5a888b9b..c5c461950 100644
--- a/apps/docs/environment-variables.mdx
+++ b/apps/docs/environment-variables.mdx
@@ -127,7 +127,7 @@ as per-task auth tokens or workspace paths.
| `ROOMOTE_FORCE_TELEMETRY` | Development only | Force-enables telemetry in development or preview environments when a Ping endpoint is explicitly configured. |
| `R_CLOUD_ENABLED` | Roomote Cloud only | Deployment-managed switch for Roomote Cloud behavior, including required anonymous analytics and Cloud support integrations. Do not set this for self-hosted deployments. |
| `R_CURATED_INTEGRATIONS_DISABLED` | Optional | Operator policy for the curated **Settings > Integrations** catalog, which is enabled by default. Set to `true` and restart Roomote to prevent those integrations from being configured or used. Existing connections remain stored while disabled and become available again once the value is unset. Communications, source-control, inference, sandbox providers, and environment-defined MCP servers are unaffected. |
-| `R_SLACK_FAST_MODE_SETTING_ENABLED` | Optional | Set to `true` and restart Roomote to expose a personal setting that defaults a user's Slack messages to fast mode without requiring `!fast`. The setting is hidden and unavailable when this flag is unset. |
+| `R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED` | Optional | Set to `true` and restart Roomote to expose a personal setting that defaults a user's supported communications messages to fast mode. The setting currently applies to Slack messages, where it removes the need for `!fast`; it is hidden and unavailable when this flag is unset. |
| `R_GITHUB_APP_SLUG` | GitHub setup | GitHub App slug used by server-rendered setup and GitHub integration flows. |
| `SETUP_TOKEN` | Required (non-local) | One-time bootstrap token that admits the first admin at `/setup`. Required on every non-local deployment — tokenless bootstrap is allowed only when `NODE_ENV` is not `production` and `R_APP_ENV` is `development`, so anything running with `NODE_ENV=production` needs it. Without it, first-admin bootstrap stays closed so nobody can claim the founding-admin slot by reaching the URL first. Optional only in local development. |
| `DASHBOARD_PASSWORD` | Production | Local fallback/admin password value used by the deployment. Generate a strong secret. |
diff --git a/apps/web/src/components/settings/UserPreferencesSection.test.tsx b/apps/web/src/components/settings/UserPreferencesSection.test.tsx
index cebbeedc4..66cf1c648 100644
--- a/apps/web/src/components/settings/UserPreferencesSection.test.tsx
+++ b/apps/web/src/components/settings/UserPreferencesSection.test.tsx
@@ -28,7 +28,7 @@ const {
setEnabled: vi.fn(),
},
personalPreferencesState: {
- preferences: { slackFastModeDefault: false },
+ preferences: { communicationsFastModeDefault: false },
isLoading: false,
isUpdating: false,
setPreferences: vi.fn(),
@@ -136,7 +136,7 @@ describe('UserPreferencesSection', () => {
narrationModeState.enabled = false;
narrationModeState.isLoading = false;
narrationModeState.isUpdating = false;
- personalPreferencesState.preferences.slackFastModeDefault = false;
+ personalPreferencesState.preferences.communicationsFastModeDefault = false;
personalPreferencesState.isLoading = false;
personalPreferencesState.isUpdating = false;
});
@@ -221,26 +221,30 @@ describe('UserPreferencesSection', () => {
);
});
- it('hides the Slack fast mode default when it is unavailable', () => {
+ it('hides the communications fast mode default when it is unavailable', () => {
render();
expect(
- screen.queryByLabelText('Toggle Slack fast mode default'),
+ screen.queryByLabelText('Toggle communications fast mode default'),
).not.toBeInTheDocument();
});
- it('updates the Slack fast mode default when it is available', () => {
- personalPreferencesState.preferences.slackFastModeDefault = true;
+ it('updates the communications fast mode default when it is available', () => {
+ personalPreferencesState.preferences.communicationsFastModeDefault = true;
- render();
+ render(
+ ,
+ );
- const toggle = screen.getByLabelText('Toggle Slack fast mode default');
+ const toggle = screen.getByLabelText(
+ 'Toggle communications fast mode default',
+ );
expect(toggle).toBeChecked();
fireEvent.click(toggle);
expect(personalPreferencesState.setPreferences).toHaveBeenCalledWith({
- slackFastModeDefault: false,
+ communicationsFastModeDefault: false,
});
});
});
diff --git a/apps/web/src/components/settings/UserPreferencesSection.tsx b/apps/web/src/components/settings/UserPreferencesSection.tsx
index 927b4089d..59afd3417 100644
--- a/apps/web/src/components/settings/UserPreferencesSection.tsx
+++ b/apps/web/src/components/settings/UserPreferencesSection.tsx
@@ -29,9 +29,9 @@ const COLOR_THEME_OPTIONS: ReadonlyArray<{
];
export function UserPreferencesSection({
- slackFastModeDefaultAvailable = false,
+ communicationsFastModeDefaultAvailable = false,
}: {
- slackFastModeDefaultAvailable?: boolean;
+ communicationsFastModeDefaultAvailable?: boolean;
}) {
const {
colorTheme,
@@ -53,12 +53,12 @@ export function UserPreferencesSection({
} = useNarrationMode();
const {
preferences,
- isLoading: isSlackFastModeDefaultLoading,
- isUpdating: isSlackFastModeDefaultUpdating,
+ isLoading: isCommunicationsFastModeDefaultLoading,
+ isUpdating: isCommunicationsFastModeDefaultUpdating,
setPreferences,
} = usePersonalPreferences({
- enabled: slackFastModeDefaultAvailable,
- errorMessage: 'Failed to update the Slack fast mode default.',
+ enabled: communicationsFastModeDefaultAvailable,
+ errorMessage: 'Failed to update the communications fast mode default.',
});
const isThemeDisabled = isThemeLoading || isThemeUpdating;
@@ -128,24 +128,25 @@ export function UserPreferencesSection({
- {slackFastModeDefaultAvailable ? (
+ {communicationsFastModeDefaultAvailable ? (
- setPreferences({ slackFastModeDefault: enabled })
+ setPreferences({ communicationsFastModeDefault: enabled })
}
/>
- Default Slack messages to fast mode
+ Default messages to fast mode
- Use fast mode for your Slack messages without adding !fast.
+ Use fast mode by default for supported communications messages.
diff --git a/apps/web/src/components/settings/pages/PersonalSettingsPage.test.tsx b/apps/web/src/components/settings/pages/PersonalSettingsPage.test.tsx
index bfdf2b04f..a04ea9d8a 100644
--- a/apps/web/src/components/settings/pages/PersonalSettingsPage.test.tsx
+++ b/apps/web/src/components/settings/pages/PersonalSettingsPage.test.tsx
@@ -53,7 +53,7 @@ describe('PersonalSettingsPage', () => {
,
);
@@ -71,7 +71,7 @@ describe('PersonalSettingsPage', () => {
,
);
diff --git a/apps/web/src/components/settings/pages/PersonalSettingsPage.tsx b/apps/web/src/components/settings/pages/PersonalSettingsPage.tsx
index d3f25e00e..5df7083b5 100644
--- a/apps/web/src/components/settings/pages/PersonalSettingsPage.tsx
+++ b/apps/web/src/components/settings/pages/PersonalSettingsPage.tsx
@@ -13,12 +13,12 @@ export function PersonalSettingsPage({
profile,
canChangePassword,
canSetPassword,
- slackFastModeDefaultAvailable,
+ communicationsFastModeDefaultAvailable,
}: {
profile: UserProfileSectionProfile;
canChangePassword: boolean;
canSetPassword: boolean;
- slackFastModeDefaultAvailable: boolean;
+ communicationsFastModeDefaultAvailable: boolean;
}) {
return (
@@ -30,7 +30,9 @@ export function PersonalSettingsPage({
) : null}
diff --git a/apps/web/src/components/settings/pages/PersonalSettingsRoute.test.tsx b/apps/web/src/components/settings/pages/PersonalSettingsRoute.test.tsx
index 6ae61ac89..48898e8a1 100644
--- a/apps/web/src/components/settings/pages/PersonalSettingsRoute.test.tsx
+++ b/apps/web/src/components/settings/pages/PersonalSettingsRoute.test.tsx
@@ -4,12 +4,12 @@ let accountCapabilities:
| {
canChangePassword: boolean;
canSetPassword: boolean;
- slackFastModeDefaultAvailable: boolean;
+ communicationsFastModeDefaultAvailable: boolean;
}
| undefined = {
canChangePassword: true,
canSetPassword: false,
- slackFastModeDefaultAvailable: true,
+ communicationsFastModeDefaultAvailable: true,
};
const { personalSettingsPageMock } = vi.hoisted(() => ({
@@ -49,7 +49,7 @@ describe('PersonalSettingsRoute', () => {
accountCapabilities = {
canChangePassword: true,
canSetPassword: false,
- slackFastModeDefaultAvailable: true,
+ communicationsFastModeDefaultAvailable: true,
};
personalSettingsPageMock.mockClear();
});
@@ -61,7 +61,7 @@ describe('PersonalSettingsRoute', () => {
{
canChangePassword: true,
canSetPassword: false,
- slackFastModeDefaultAvailable: true,
+ communicationsFastModeDefaultAvailable: true,
profile: {
email: 'ada@example.com',
imageUrl: 'https://example.com/ada.png',
diff --git a/apps/web/src/components/settings/pages/PersonalSettingsRoute.tsx b/apps/web/src/components/settings/pages/PersonalSettingsRoute.tsx
index f89cfcf41..a161f223c 100644
--- a/apps/web/src/components/settings/pages/PersonalSettingsRoute.tsx
+++ b/apps/web/src/components/settings/pages/PersonalSettingsRoute.tsx
@@ -18,8 +18,9 @@ export function PersonalSettingsRoute() {
{
colorTheme: 'system',
mindReaderMode: false,
narrationMode: false,
- slackFastModeDefault: false,
+ communicationsFastModeDefault: false,
});
});
diff --git a/apps/web/src/hooks/usePersonalPreferences.ts b/apps/web/src/hooks/usePersonalPreferences.ts
index c85ad891f..b559d46cf 100644
--- a/apps/web/src/hooks/usePersonalPreferences.ts
+++ b/apps/web/src/hooks/usePersonalPreferences.ts
@@ -51,10 +51,10 @@ function mergeResultForUpdatedFields(
updates.narrationMode === undefined
? mergedPreferences.narrationMode
: result.narrationMode,
- slackFastModeDefault:
- updates.slackFastModeDefault === undefined
- ? mergedPreferences.slackFastModeDefault
- : result.slackFastModeDefault,
+ communicationsFastModeDefault:
+ updates.communicationsFastModeDefault === undefined
+ ? mergedPreferences.communicationsFastModeDefault
+ : result.communicationsFastModeDefault,
};
}
@@ -85,12 +85,12 @@ function rollbackUpdatedFields(
mergedPreferences.narrationMode === optimisticPreferences.narrationMode
? previousPreferences.narrationMode
: mergedPreferences.narrationMode,
- slackFastModeDefault:
- updates.slackFastModeDefault !== undefined &&
- mergedPreferences.slackFastModeDefault ===
- optimisticPreferences.slackFastModeDefault
- ? previousPreferences.slackFastModeDefault
- : mergedPreferences.slackFastModeDefault,
+ communicationsFastModeDefault:
+ updates.communicationsFastModeDefault !== undefined &&
+ mergedPreferences.communicationsFastModeDefault ===
+ optimisticPreferences.communicationsFastModeDefault
+ ? previousPreferences.communicationsFastModeDefault
+ : mergedPreferences.communicationsFastModeDefault,
};
}
diff --git a/apps/web/src/trpc/commands/preferences/index.test.ts b/apps/web/src/trpc/commands/preferences/index.test.ts
index c36610270..dbeff15a5 100644
--- a/apps/web/src/trpc/commands/preferences/index.test.ts
+++ b/apps/web/src/trpc/commands/preferences/index.test.ts
@@ -41,7 +41,7 @@ describe('personal account capabilities', () => {
await expect(getPersonalAccountCapabilitiesCommand(auth)).resolves.toEqual({
canChangePassword: false,
canSetPassword: true,
- slackFastModeDefaultAvailable: false,
+ communicationsFastModeDefaultAvailable: false,
});
});
@@ -51,7 +51,7 @@ describe('personal account capabilities', () => {
await expect(getPersonalAccountCapabilitiesCommand(auth)).resolves.toEqual({
canChangePassword: true,
canSetPassword: false,
- slackFastModeDefaultAvailable: false,
+ communicationsFastModeDefaultAvailable: false,
});
});
diff --git a/apps/web/src/trpc/commands/preferences/index.ts b/apps/web/src/trpc/commands/preferences/index.ts
index 5e1825941..9b7fd12d9 100644
--- a/apps/web/src/trpc/commands/preferences/index.ts
+++ b/apps/web/src/trpc/commands/preferences/index.ts
@@ -37,9 +37,9 @@ function normalizePersonalPreferences(
typeof metadata.narration_mode === 'boolean'
? metadata.narration_mode
: DEFAULT_PERSONAL_PREFERENCES.narrationMode,
- slackFastModeDefault:
- Env.R_SLACK_FAST_MODE_SETTING_ENABLED === true &&
- metadata.slack_fast_mode_default === true,
+ communicationsFastModeDefault:
+ Env.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED === true &&
+ metadata.communications_fast_mode_default === true,
};
}
@@ -66,8 +66,8 @@ export async function getPersonalAccountCapabilitiesCommand(
return {
canChangePassword: hasCredentialAccount,
canSetPassword: !hasCredentialAccount,
- slackFastModeDefaultAvailable:
- Env.R_SLACK_FAST_MODE_SETTING_ENABLED === true,
+ communicationsFastModeDefaultAvailable:
+ Env.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED === true,
};
}
@@ -120,11 +120,11 @@ export async function updatePersonalPreferencesCommand(
const nextMetadataRecord: UserMetadataRecord = {};
if (
- input.slackFastModeDefault !== undefined &&
- Env.R_SLACK_FAST_MODE_SETTING_ENABLED !== true
+ input.communicationsFastModeDefault !== undefined &&
+ Env.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED !== true
) {
throw new Error(
- 'The Slack fast mode default setting is not enabled for this deployment.',
+ 'The communications fast mode default setting is not enabled for this deployment.',
);
}
@@ -140,8 +140,9 @@ export async function updatePersonalPreferencesCommand(
nextMetadataRecord.narration_mode = input.narrationMode;
}
- if (input.slackFastModeDefault !== undefined) {
- nextMetadataRecord.slack_fast_mode_default = input.slackFastModeDefault;
+ if (input.communicationsFastModeDefault !== undefined) {
+ nextMetadataRecord.communications_fast_mode_default =
+ input.communicationsFastModeDefault;
}
if (Object.keys(nextMetadataRecord).length === 0) {
diff --git a/apps/web/src/trpc/commands/preferences/personal-preferences.test.ts b/apps/web/src/trpc/commands/preferences/personal-preferences.test.ts
index 1cdbcf6ef..fe6c9b24d 100644
--- a/apps/web/src/trpc/commands/preferences/personal-preferences.test.ts
+++ b/apps/web/src/trpc/commands/preferences/personal-preferences.test.ts
@@ -3,7 +3,7 @@ import { db, eq, userFactory, users } from '@roomote/db/server';
import type { UserAuthSuccess } from '@/types';
const { mockEnv } = vi.hoisted(() => ({
- mockEnv: { R_SLACK_FAST_MODE_SETTING_ENABLED: false },
+ mockEnv: { R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED: false },
}));
vi.mock('@/lib/server/env', () => ({ Env: mockEnv }));
@@ -19,7 +19,7 @@ function buildAuth(userId: string) {
describe('personal preferences', () => {
beforeEach(() => {
- mockEnv.R_SLACK_FAST_MODE_SETTING_ENABLED = false;
+ mockEnv.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED = false;
});
it('defaults mind reader mode to disabled', async () => {
@@ -71,39 +71,41 @@ describe('personal preferences', () => {
);
});
- it('rejects Slack fast mode updates when the deployment setting is disabled', async () => {
+ it('rejects communications fast mode updates when the deployment setting is disabled', async () => {
const user = await userFactory.create();
await expect(
updatePersonalPreferencesCommand(buildAuth(user.id), {
- slackFastModeDefault: true,
+ communicationsFastModeDefault: true,
}),
).rejects.toThrow(
- 'The Slack fast mode default setting is not enabled for this deployment.',
+ 'The communications fast mode default setting is not enabled for this deployment.',
);
});
- it('does not expose a stored Slack fast mode default when the deployment setting is disabled', async () => {
+ it('does not expose a stored communications fast mode default when the deployment setting is disabled', async () => {
const user = await userFactory.create({
- metadata: { slack_fast_mode_default: true },
+ metadata: { communications_fast_mode_default: true },
});
await expect(
getPersonalPreferencesCommand(buildAuth(user.id)),
).resolves.toEqual(
- expect.objectContaining({ slackFastModeDefault: false }),
+ expect.objectContaining({ communicationsFastModeDefault: false }),
);
});
- it('persists the Slack fast mode default when the deployment setting is enabled', async () => {
- mockEnv.R_SLACK_FAST_MODE_SETTING_ENABLED = true;
+ it('persists the communications fast mode default when the deployment setting is enabled', async () => {
+ mockEnv.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED = true;
const user = await userFactory.create();
await expect(
updatePersonalPreferencesCommand(buildAuth(user.id), {
- slackFastModeDefault: true,
+ communicationsFastModeDefault: true,
}),
- ).resolves.toEqual(expect.objectContaining({ slackFastModeDefault: true }));
+ ).resolves.toEqual(
+ expect.objectContaining({ communicationsFastModeDefault: true }),
+ );
const storedUser = await db.query.users.findFirst({
where: eq(users.id, user.id),
@@ -111,7 +113,7 @@ describe('personal preferences', () => {
});
expect(storedUser?.metadata).toEqual(
- expect.objectContaining({ slack_fast_mode_default: true }),
+ expect.objectContaining({ communications_fast_mode_default: true }),
);
});
});
diff --git a/apps/web/src/trpc/routers/_app.ts b/apps/web/src/trpc/routers/_app.ts
index eda088036..ecd12d954 100644
--- a/apps/web/src/trpc/routers/_app.ts
+++ b/apps/web/src/trpc/routers/_app.ts
@@ -1432,14 +1432,14 @@ export const appRouter = createRouter({
colorTheme: z.enum(PERSONAL_COLOR_THEMES).optional(),
mindReaderMode: z.boolean().optional(),
narrationMode: z.boolean().optional(),
- slackFastModeDefault: z.boolean().optional(),
+ communicationsFastModeDefault: z.boolean().optional(),
})
.refine(
(input) =>
input.colorTheme !== undefined ||
input.mindReaderMode !== undefined ||
input.narrationMode !== undefined ||
- input.slackFastModeDefault !== undefined,
+ input.communicationsFastModeDefault !== undefined,
{
message: 'Expected at least one personal preference to update.',
},
diff --git a/apps/web/src/types/preferences.ts b/apps/web/src/types/preferences.ts
index e0d1db788..60dd3a4f0 100644
--- a/apps/web/src/types/preferences.ts
+++ b/apps/web/src/types/preferences.ts
@@ -16,7 +16,7 @@ export interface PersonalPreferences {
colorTheme: PersonalColorTheme;
mindReaderMode: boolean;
narrationMode: boolean;
- slackFastModeDefault: boolean;
+ communicationsFastModeDefault: boolean;
}
export type PersonalPreferencesUpdate = Partial;
@@ -25,5 +25,5 @@ export const DEFAULT_PERSONAL_PREFERENCES: PersonalPreferences = {
colorTheme: 'system',
mindReaderMode: false,
narrationMode: false,
- slackFastModeDefault: false,
+ communicationsFastModeDefault: false,
};
diff --git a/docker-compose.production.yml b/docker-compose.production.yml
index 8aacfe159..1e4d678e0 100644
--- a/docker-compose.production.yml
+++ b/docker-compose.production.yml
@@ -23,7 +23,7 @@ x-roomote-production-env: &roomote-production-env
R_PUBLIC_URL: https://${ROOMOTE_APP_DOMAIN:?ROOMOTE_APP_DOMAIN is required}
R_INSTANCE_ID: ${R_INSTANCE_ID:-}
R_CURATED_INTEGRATIONS_DISABLED: ${R_CURATED_INTEGRATIONS_DISABLED:-false}
- R_SLACK_FAST_MODE_SETTING_ENABLED: ${R_SLACK_FAST_MODE_SETTING_ENABLED:-false}
+ R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED: ${R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED:-false}
TRPC_URL: http://api:3001
PREVIEW_PROXY_BASE_URL: https://${ROOMOTE_PREVIEW_DOMAIN:?ROOMOTE_PREVIEW_DOMAIN is required}
NEXT_PUBLIC_PREVIEW_PROXY_BASE_URL: https://${ROOMOTE_PREVIEW_DOMAIN:?ROOMOTE_PREVIEW_DOMAIN is required}
diff --git a/docker-compose.self-host.yml b/docker-compose.self-host.yml
index b507f98fe..5325eb7eb 100644
--- a/docker-compose.self-host.yml
+++ b/docker-compose.self-host.yml
@@ -22,7 +22,7 @@ x-roomote-env: &roomote-env
R_APP_URL: ${R_PUBLIC_URL:-http://localhost:13000}
R_INSTANCE_ID: ${R_INSTANCE_ID:-}
R_CURATED_INTEGRATIONS_DISABLED: ${R_CURATED_INTEGRATIONS_DISABLED:-false}
- R_SLACK_FAST_MODE_SETTING_ENABLED: ${R_SLACK_FAST_MODE_SETTING_ENABLED:-false}
+ R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED: ${R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED:-false}
# The Brain: supplying this key gives the deployment shared memory that
# agents consult. It powers the brain service's embeddings and is the
# single activation signal — there is no Settings UI for it.
diff --git a/packages/env/src/__tests__/index.test.ts b/packages/env/src/__tests__/index.test.ts
index a72d0097a..b1bacc8b0 100644
--- a/packages/env/src/__tests__/index.test.ts
+++ b/packages/env/src/__tests__/index.test.ts
@@ -226,19 +226,19 @@ describe('Env', () => {
expect(areCuratedIntegrationsDisabled('0')).toBe(false);
});
- it('keeps the Slack fast mode setting opt-in', () => {
+ it('keeps the communications fast mode setting opt-in', () => {
const runtimeEnv = { ...process.env };
delete runtimeEnv.SKIP_ENV_VALIDATION;
- delete runtimeEnv.R_SLACK_FAST_MODE_SETTING_ENABLED;
+ delete runtimeEnv.R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED;
- expect(createRoomoteEnv(runtimeEnv).R_SLACK_FAST_MODE_SETTING_ENABLED).toBe(
- false,
- );
+ expect(
+ createRoomoteEnv(runtimeEnv).R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED,
+ ).toBe(false);
expect(
createRoomoteEnv({
...runtimeEnv,
- R_SLACK_FAST_MODE_SETTING_ENABLED: 'true',
- }).R_SLACK_FAST_MODE_SETTING_ENABLED,
+ R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED: 'true',
+ }).R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED,
).toBe(true);
});
diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts
index 3bf0e2db4..febb8520b 100644
--- a/packages/env/src/index.ts
+++ b/packages/env/src/index.ts
@@ -124,8 +124,8 @@ const serverSchema = {
// Roomote Cloud-only analytics and support integrations. These values are
// intentionally not used by self-hosted deployments.
R_CLOUD_ENABLED: optInBoolean(),
- // Exposes the per-user setting that defaults Slack messages to fast mode.
- R_SLACK_FAST_MODE_SETTING_ENABLED: optInBoolean(),
+ // Exposes the per-user setting that defaults communications messages to fast mode.
+ R_COMMUNICATIONS_FAST_MODE_SETTING_ENABLED: optInBoolean(),
// Operator policy for the curated Settings > Integrations catalog. Enabled
// by default; operators opt out explicitly. Existing connections remain
// stored but cannot be configured or used while disabled.