diff --git a/apps/api/src/handlers/github/__tests__/notifyPullRequestTerminalStatus.test.ts b/apps/api/src/handlers/github/__tests__/notifyPullRequestTerminalStatus.test.ts index 409ec7a3..10e3562b 100644 --- a/apps/api/src/handlers/github/__tests__/notifyPullRequestTerminalStatus.test.ts +++ b/apps/api/src/handlers/github/__tests__/notifyPullRequestTerminalStatus.test.ts @@ -26,7 +26,6 @@ const { mockResolveSlackReactionNames: vi.fn().mockResolvedValue({ ackEmoji: 'eyes', completionEmoji: 'white_check_mark', - summonEmoji: null, }), mockStickyFooterPost: vi.fn().mockResolvedValue('msg-ts-123'), mockGetCommunicationProviderAdapter: vi.fn(), @@ -147,7 +146,6 @@ describe('notifyPullRequestTerminalStatus', () => { mockResolveSlackReactionNames.mockResolvedValue({ ackEmoji: 'eyes', completionEmoji: 'white_check_mark', - summonEmoji: null, }); mockStickyFooterPost.mockResolvedValue('msg-ts-123'); mockPostMessage.mockResolvedValue({ diff --git a/apps/api/src/handlers/slack/events/message-entry.ts b/apps/api/src/handlers/slack/events/message-entry.ts index f8434034..71c9d31c 100644 --- a/apps/api/src/handlers/slack/events/message-entry.ts +++ b/apps/api/src/handlers/slack/events/message-entry.ts @@ -1587,7 +1587,7 @@ async function isSlackEvalLauncherEnabled(): Promise { ); } -export async function handleSlackEntryEvent(params: { +async function handleSlackEntryEvent(params: { event: SlackEvent; slackInstallation: SlackInstallation; slack: SlackNotifier; diff --git a/apps/api/src/handlers/slack/events/reactions.ts b/apps/api/src/handlers/slack/events/reactions.ts index 9a765ed4..44d3c8b6 100644 --- a/apps/api/src/handlers/slack/events/reactions.ts +++ b/apps/api/src/handlers/slack/events/reactions.ts @@ -39,13 +39,9 @@ import { TASK_SUGGESTION_TYPES, } from '../constants.js'; import type { SlackWebhookContext } from '../context.js'; -import { - isConfiguredSlackReaction, - isThumbsUpReaction, -} from '../helpers/event-normalization.js'; +import { isThumbsUpReaction } from '../helpers/event-normalization.js'; import { postTaskSuggestionStartedMessage } from '../helpers/thread-posting.js'; import { lookupSlackUserMapping } from '../helpers/user-mapping.js'; -import { handleSlackEntryEvent } from './message-entry.js'; import { runTaskSuggestionReactionContention, type TaskSuggestionReactionLaunchResult, @@ -715,12 +711,6 @@ export async function handleReactionAddedEvent(params: { return; } - const isThumbsUp = isThumbsUpReaction(event.reaction); - const isSummonReaction = isConfiguredSlackReaction( - event.reaction, - reactionNames.summonEmoji, - ); - if (event.user === context.slackInstallation.botUserId) { apiLogger.debug( `[SlackWebhook] Ignoring self-reaction from bot user ${event.user} on ${event.item.channel}:${event.item.ts}`, @@ -728,90 +718,36 @@ export async function handleReactionAddedEvent(params: { return; } - if (isThumbsUp) { - apiLogger.debug( - `[SetupSuggestionLifecycle] Processing thumbs-up reaction team=${context.teamId} channel=${event.item.channel} messageTs=${event.item.ts} reaction=${event.reaction} user=${event.user}`, - ); - const setupSuggestionLockKey = `${SLACK_SETUP_SUGGESTION_LOCK_PREFIX}${event.item.channel}:${event.item.ts}`; - const setupSuggestionHandled = await launchTaskSuggestionTaskWithContention( - { - lockKey: setupSuggestionLockKey, - channelId: event.item.channel, - messageTs: event.item.ts, - launch: () => - launchTaskSuggestionTaskFromReaction({ - teamId: context.teamId, - slack: context.slack, - reactionEvent: event, - ackEmoji: reactionNames.ackEmoji, - completionEmoji: reactionNames.completionEmoji, - }), - }, - ); - - if (setupSuggestionHandled) { - apiLogger.debug( - `[SlackWebhook] Setup suggestion reaction handled for ${event.item.channel}:${event.item.ts}`, - ); - return; - } - - apiLogger.debug( - `[SlackWebhook] Reaction no-op for ${event.item.channel}:${event.item.ts}`, - ); - } - - if (!isSummonReaction) { + if (!isThumbsUpReaction(event.reaction)) { return; } - const sourceMessage = await context.slack.getMessage({ - channel: event.item.channel, + apiLogger.debug( + `[SetupSuggestionLifecycle] Processing thumbs-up reaction team=${context.teamId} channel=${event.item.channel} messageTs=${event.item.ts} reaction=${event.reaction} user=${event.user}`, + ); + const setupSuggestionLockKey = `${SLACK_SETUP_SUGGESTION_LOCK_PREFIX}${event.item.channel}:${event.item.ts}`; + const setupSuggestionHandled = await launchTaskSuggestionTaskWithContention({ + lockKey: setupSuggestionLockKey, + channelId: event.item.channel, messageTs: event.item.ts, + launch: () => + launchTaskSuggestionTaskFromReaction({ + teamId: context.teamId, + slack: context.slack, + reactionEvent: event, + ackEmoji: reactionNames.ackEmoji, + completionEmoji: reactionNames.completionEmoji, + }), }); - if (!sourceMessage) { + if (setupSuggestionHandled) { apiLogger.debug( - `[SlackWebhook] Summon reaction source message unavailable for ${event.item.channel}:${event.item.ts}`, + `[SlackWebhook] Setup suggestion reaction handled for ${event.item.channel}:${event.item.ts}`, ); return; } - const isRoomoteAuthoredBotMessage = - sourceMessage.user === context.slackInstallation.botUserId || - sourceMessage.app_id === context.slackInstallation.appId; - - if (isRoomoteAuthoredBotMessage) { - apiLogger.debug( - `[SlackWebhook] Ignoring summon reaction on Roomote-authored bot message ${event.item.channel}:${event.item.ts}`, - ); - return; - } - - if (sourceMessage.text.trim().length === 0 && !sourceMessage.files?.length) { - apiLogger.debug( - `[SlackWebhook] Ignoring summon reaction on empty source message ${event.item.channel}:${event.item.ts}`, - ); - return; - } - - await handleSlackEntryEvent({ - event: { - type: 'message', - channel: event.item.channel, - user: event.user, - text: sourceMessage.text, - ts: sourceMessage.ts, - thread_ts: sourceMessage.thread_ts ?? sourceMessage.ts, - app_id: sourceMessage.app_id, - files: sourceMessage.files, - attachments: sourceMessage.attachments, - }, - slackInstallation: context.slackInstallation as never, - slack: context.slack, - teamId: context.teamId, - ackEmoji: reactionNames.ackEmoji, - completionEmoji: reactionNames.completionEmoji, - skipThreadFollowupHandling: true, - }); + apiLogger.debug( + `[SlackWebhook] Reaction no-op for ${event.item.channel}:${event.item.ts}`, + ); } diff --git a/apps/api/src/handlers/slack/helpers/event-normalization.ts b/apps/api/src/handlers/slack/helpers/event-normalization.ts index 3064984a..6d93394e 100644 --- a/apps/api/src/handlers/slack/helpers/event-normalization.ts +++ b/apps/api/src/handlers/slack/helpers/event-normalization.ts @@ -20,17 +20,6 @@ export function isThumbsUpReaction(reaction: string): boolean { return THUMBS_UP_REACTIONS.has(normalizedReaction); } -export function isConfiguredSlackReaction( - reaction: string, - configuredEmoji: string | null | undefined, -): boolean { - if (!configuredEmoji) { - return false; - } - - return normalizeSlackReactionName(reaction) === configuredEmoji; -} - function coerceSlackWorkflowInputToString(value: unknown): string | undefined { if (typeof value === 'string') { const trimmed = value.trim(); diff --git a/apps/docs/AGENTS.md b/apps/docs/AGENTS.md index c253d7f3..db5bf6dc 100644 --- a/apps/docs/AGENTS.md +++ b/apps/docs/AGENTS.md @@ -11,7 +11,7 @@ Public, user-facing documentation: - setup and self-hosting - product concepts and how Roomote works - admin and configuration workflows (dashboard, environments, skills, - automations, integrations, vibes, personal settings) + automations, integrations, personal settings) - integrations and common user-facing tasks ## What does NOT belong here diff --git a/apps/docs/docs.json b/apps/docs/docs.json index d36d2bec..6e7c3ca3 100644 --- a/apps/docs/docs.json +++ b/apps/docs/docs.json @@ -113,7 +113,6 @@ "skills", "automations", "users", - "vibes", "anonymous-analytics", "personal-settings" ] diff --git a/apps/docs/vibes.mdx b/apps/docs/vibes.mdx deleted file mode 100644 index 0671de96..00000000 --- a/apps/docs/vibes.mdx +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Vibes -icon: rainbow -description: Tune Roomote Slack reactions, summon emoji, and team-wide tone settings. ---- - -Vibes controls the small signals that make Roomote feel like it belongs in -your team workflow. - -Use this page when you want to change how Roomote reacts in Slack, let -teammates start tasks with an emoji reaction, or adjust the tone Roomote uses -in team-facing messages. - -## What you can configure - -| Setting | What it controls | Example | -| ------------------------- | --------------------------------------------------------------------------- | ------------------------ | -| **Acknowledgement emoji** | The reaction Roomote uses to show it saw a Slack message | `eyes` | -| **Completion emoji** | The reaction Roomote can use when supported work finishes | `white_check_mark` | -| **Summon emoji** | The reaction teammates can add to an existing Slack message to start a task | `shipit` | -| **Style guidance** | Optional guidance for how Roomote should sound in messages | "Direct, concise, calm." | - -Use Slack emoji names, not pasted emoji characters. Roomote accepts names with -or without surrounding colons, so `shipit` and `:shipit:` both describe the -same Slack reaction. - -## Call Roomote with a reaction - -A summon emoji lets a teammate react to an existing Slack message and turn -that message into the starting point for a Roomote task. - -This is useful when: - -- someone reports a bug with enough context in a channel -- a teammate asks a codebase question that Roomote can investigate -- a thread contains a request you want Roomote to pick up without rewriting it - -Pick an emoji the team will not use casually. If the emoji is common, Roomote -may start tasks from reactions that were only meant as normal Slack feedback. -Tell the team what the summon emoji means before turning it on in busy -channels. - -## Choose reaction behavior - -The acknowledgement reaction is the quick "I saw this" signal. The completion -reaction is for supported completion moments where a Slack reaction is the -right confirmation. - -If teammates are missing Roomote's status updates, choose reactions that stand -out in your workspace. If channels feel too busy, keep the defaults or use -quieter custom emoji. - -## Add style guidance - -Use style guidance for tone, not operating policy. - -Good style guidance: - -```text -Keep status updates short and direct. Use plain language. Avoid hype. -``` - -Not-so-good style guidance: - -```text -Always approve PRs. -Never ask follow-up questions. -Use this secret token when tests fail. -``` - -Use [Agent Guidance](/agent-guidance) for team-wide work instructions, -and use [Environments](/environments) for repository-specific setup and -verification rules. - -## Download starter emoji - -The Vibes page includes a few starter Roomote emoji images you can download -and upload to Slack. Use them as a quick starting point if your team wants a -dedicated summon emoji instead of reusing an existing workspace reaction. - -## Common issues - -- **The summon reaction does not start a task.** Check that Slack is - connected, Roomote is in the channel, and the reaction name matches the - saved summon emoji. -- **Roomote reacts with the default emoji.** Save the new emoji name and try - again in a fresh Slack message. -- **The tone feels over-corrected.** Shorten the style guidance. A few clear - sentences work better than a long style guide. -- **A channel gets noisy.** Pick a less common summon emoji or ask the team to - use mentions for requests that need more context. diff --git a/apps/web/public/vibes/ideas/let-me-roomote-that-for-you.png b/apps/web/public/vibes/ideas/let-me-roomote-that-for-you.png deleted file mode 100644 index 4157573d..00000000 Binary files a/apps/web/public/vibes/ideas/let-me-roomote-that-for-you.png and /dev/null differ diff --git a/apps/web/public/vibes/ideas/party-parrot.png b/apps/web/public/vibes/ideas/party-parrot.png deleted file mode 100644 index ea8cf511..00000000 Binary files a/apps/web/public/vibes/ideas/party-parrot.png and /dev/null differ diff --git a/apps/web/public/vibes/ideas/roomote_right_black.png b/apps/web/public/vibes/ideas/roomote_right_black.png deleted file mode 100644 index 598dff33..00000000 Binary files a/apps/web/public/vibes/ideas/roomote_right_black.png and /dev/null differ diff --git a/apps/web/public/vibes/ideas/roomote_right_green.png b/apps/web/public/vibes/ideas/roomote_right_green.png deleted file mode 100644 index 515abe99..00000000 Binary files a/apps/web/public/vibes/ideas/roomote_right_green.png and /dev/null differ diff --git a/apps/web/public/vibes/ideas/roomote_up_black.png b/apps/web/public/vibes/ideas/roomote_up_black.png deleted file mode 100644 index 5c6f81c7..00000000 Binary files a/apps/web/public/vibes/ideas/roomote_up_black.png and /dev/null differ diff --git a/apps/web/public/vibes/ideas/roomote_up_green.png b/apps/web/public/vibes/ideas/roomote_up_green.png deleted file mode 100644 index d76575c2..00000000 Binary files a/apps/web/public/vibes/ideas/roomote_up_green.png and /dev/null differ diff --git a/apps/web/public/vibes/ideas/ship-it.png b/apps/web/public/vibes/ideas/ship-it.png deleted file mode 100644 index b80e43e8..00000000 Binary files a/apps/web/public/vibes/ideas/ship-it.png and /dev/null differ diff --git a/apps/web/public/vibes/ideas/sparkles.png b/apps/web/public/vibes/ideas/sparkles.png deleted file mode 100644 index f98238f1..00000000 Binary files a/apps/web/public/vibes/ideas/sparkles.png and /dev/null differ diff --git a/apps/web/src/app/(authenticated)/settings/vibes/page.client.test.tsx b/apps/web/src/app/(authenticated)/settings/vibes/page.client.test.tsx deleted file mode 100644 index 74df761f..00000000 --- a/apps/web/src/app/(authenticated)/settings/vibes/page.client.test.tsx +++ /dev/null @@ -1,14 +0,0 @@ -vi.mock('@/components/settings/pages/VibesSettingsPage', () => ({ - VibesSettingsPage: () =>
Vibes Settings Page
, -})); - -import Page from './page'; -import { render, screen } from '@testing-library/react'; - -describe('settings vibes page', () => { - it('renders the vibes settings page wrapper', () => { - render(); - - expect(screen.getByText('Vibes Settings Page')).toBeInTheDocument(); - }); -}); diff --git a/apps/web/src/app/(authenticated)/settings/vibes/page.tsx b/apps/web/src/app/(authenticated)/settings/vibes/page.tsx deleted file mode 100644 index db9c1ee2..00000000 --- a/apps/web/src/app/(authenticated)/settings/vibes/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { VibesSettingsPage } from '@/components/settings/pages/VibesSettingsPage'; - -export default function Page() { - return ; -} diff --git a/apps/web/src/components/settings/SettingsShell.client.test.tsx b/apps/web/src/components/settings/SettingsShell.client.test.tsx index faf27e93..94d9f440 100644 --- a/apps/web/src/components/settings/SettingsShell.client.test.tsx +++ b/apps/web/src/components/settings/SettingsShell.client.test.tsx @@ -54,7 +54,6 @@ vi.mock('@/components/system', async () => { Label: ({ children }: { children: ReactNode }) => , PlugIcon: Icon, GraduationCap: Icon, - Rainbow: Icon, ScrollText: Icon, Select: ({ children }: { children: ReactNode }) =>
{children}
, SelectContent: ({ children }: { children: ReactNode }) => ( @@ -101,7 +100,6 @@ describe('SettingsShell', () => { ).toBeInTheDocument(); expect(screen.getByRole('link', { name: /users/i })).toBeInTheDocument(); expect(screen.getByRole('link', { name: /skills/i })).toBeInTheDocument(); - expect(screen.getByRole('link', { name: /vibes/i })).toBeInTheDocument(); expect( screen.getByText( 'Enable deployment integrations. Individual users can optionally link their own accounts when an integration supports it.', diff --git a/apps/web/src/components/settings/VibesSettings.client.test.tsx b/apps/web/src/components/settings/VibesSettings.client.test.tsx deleted file mode 100644 index 48fd5ce3..00000000 --- a/apps/web/src/components/settings/VibesSettings.client.test.tsx +++ /dev/null @@ -1,585 +0,0 @@ -import type { - ButtonHTMLAttributes, - ChangeEvent, - FocusEvent, - HTMLAttributes, - InputHTMLAttributes, - LabelHTMLAttributes, - ReactNode, - SVGProps, - TextareaHTMLAttributes, -} from 'react'; -import { - act, - fireEvent, - render, - screen, - waitFor, -} from '@testing-library/react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; - -const { - inputPropsById, - textareaPropsById, - mockSettingsState, - mockUpdateVibesSettings, - toastErrorMock, - toastSuccessMock, -} = vi.hoisted(() => ({ - inputPropsById: { - current: {} as Record>, - }, - textareaPropsById: { - current: {} as Record>, - }, - mockSettingsState: { - current: { - slackSummonEmoji: null as string | null, - slackAckEmoji: 'eyes', - slackCompletionEmoji: 'white_check_mark', - styleGuidance: null as string | null, - defaults: { - slackAckEmoji: 'eyes', - slackCompletionEmoji: 'white_check_mark', - }, - }, - }, - mockUpdateVibesSettings: vi.fn( - async (input: { - slackSummonEmoji?: string | null; - slackAckEmoji?: string; - slackCompletionEmoji?: string; - styleGuidance?: string | null; - }): Promise< - | { - success: true; - settings: typeof mockSettingsState.current; - } - | { - success: false; - fieldErrors: Partial< - Record< - 'slackAckEmoji' | 'slackCompletionEmoji' | 'styleGuidance', - string - > - >; - } - > => { - const normalize = (value: string | null | undefined) => { - const trimmed = value - ?.trim() - .replace(/^:+|:+$/g, '') - .toLowerCase(); - return trimmed ? trimmed : null; - }; - - if ( - Object.prototype.hasOwnProperty.call(input, 'slackAckEmoji') && - !normalize(input.slackAckEmoji) - ) { - return { - success: false as const, - fieldErrors: { - slackAckEmoji: 'Acknowledgement emoji is required.', - }, - }; - } - - if ( - Object.prototype.hasOwnProperty.call(input, 'slackCompletionEmoji') && - !normalize(input.slackCompletionEmoji) - ) { - return { - success: false as const, - fieldErrors: { - slackCompletionEmoji: 'Completion emoji is required.', - }, - }; - } - - mockSettingsState.current = { - ...mockSettingsState.current, - ...(Object.prototype.hasOwnProperty.call(input, 'slackSummonEmoji') - ? { slackSummonEmoji: normalize(input.slackSummonEmoji) } - : {}), - ...(Object.prototype.hasOwnProperty.call(input, 'slackAckEmoji') - ? { slackAckEmoji: normalize(input.slackAckEmoji) ?? 'eyes' } - : {}), - ...(Object.prototype.hasOwnProperty.call(input, 'slackCompletionEmoji') - ? { - slackCompletionEmoji: - normalize(input.slackCompletionEmoji) ?? 'white_check_mark', - } - : {}), - ...(Object.prototype.hasOwnProperty.call(input, 'styleGuidance') - ? { styleGuidance: input.styleGuidance?.trim() || null } - : {}), - }; - - return { - success: true as const, - settings: mockSettingsState.current, - }; - }, - ), - toastErrorMock: vi.fn(), - toastSuccessMock: vi.fn(), -})); - -vi.mock('sonner', () => ({ - toast: { - error: toastErrorMock, - success: toastSuccessMock, - }, -})); - -vi.mock('@/trpc/client', () => ({ - useTRPC: () => ({ - vibes: { - get: { - queryKey: () => ['vibes'], - queryOptions: () => ({ - queryKey: ['vibes'], - queryFn: async () => mockSettingsState.current, - }), - }, - update: { - mutationOptions: (options = {}) => ({ - mutationFn: mockUpdateVibesSettings, - ...options, - }), - }, - }, - }), -})); - -vi.mock('@/components/settings', () => ({ - Section: ({ - title, - children, - footer, - }: { - title: ReactNode; - children: ReactNode; - footer?: ReactNode; - }) => ( -
-

{title}

-
{children}
- {footer ?
{footer}
: null} -
- ), -})); - -vi.mock('@/components/system', () => ({ - AlertCircle: (props: SVGProps) => , - Button: ({ - children, - ...props - }: { children: ReactNode } & ButtonHTMLAttributes) => ( - - ), - Check: (props: SVGProps) => , - MessageSquareHeart: (props: SVGProps) => , - Input: (props: InputHTMLAttributes) => { - if (props.id) { - inputPropsById.current[props.id] = props; - } - - return ; - }, - Label: ({ - children, - ...props - }: { children: ReactNode } & LabelHTMLAttributes) => ( - - ), - RotateCcw: (props: SVGProps) => , - Skeleton: (props: HTMLAttributes) =>
, - Smile: (props: SVGProps) => , - Spinner: (props: HTMLAttributes) => , - Sun: (props: SVGProps) => , - Textarea: (props: TextareaHTMLAttributes) => { - if (props.id) { - textareaPropsById.current[props.id] = props; - } - - return