From 1edfc471838505c0be1b958ab3d54cf01c2df9cc Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 22 May 2026 08:23:12 -0700 Subject: [PATCH 1/4] fix(combobox): show selected values in multi-select trigger label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The collapsed trigger was reading only `selectedOption` (the single-value path) and falling back to the placeholder when nothing matched, so a multi-select dropdown with 1+ checked items still rendered "Select one or more channels" instead of the actual selections. Added `multiSelectLabel` derived from `multiSelectValues`: - 1 value → that label - 2 values → "A, B" - 3+ → "A, B +N" Trigger now prefers `multiSelectLabel` when present and falls back to the single-select label / placeholder otherwise. Muted-text color also flips off when multi has any selection. --- .../emcn/components/combobox/combobox.tsx | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/sim/components/emcn/components/combobox/combobox.tsx b/apps/sim/components/emcn/components/combobox/combobox.tsx index f423f7fa939..5172d5db337 100644 --- a/apps/sim/components/emcn/components/combobox/combobox.tsx +++ b/apps/sim/components/emcn/components/combobox/combobox.tsx @@ -214,6 +214,22 @@ const Combobox = memo( [allOptions, effectiveSelectedValue] ) + /** + * Label rendered in the collapsed trigger for multi-select mode. + * Shows the single label when one value is picked, comma-joined labels + * for two, or "first, second +N" when more are selected. Falls back to + * the raw value if an option for it hasn't loaded yet. + */ + const multiSelectLabel = useMemo(() => { + if (!multiSelect || !multiSelectValues || multiSelectValues.length === 0) return null + const labelFor = (v: string) => allOptions.find((opt) => opt.value === v)?.label ?? v + if (multiSelectValues.length === 1) return labelFor(multiSelectValues[0]) + if (multiSelectValues.length === 2) { + return `${labelFor(multiSelectValues[0])}, ${labelFor(multiSelectValues[1])}` + } + return `${labelFor(multiSelectValues[0])}, ${labelFor(multiSelectValues[1])} +${multiSelectValues.length - 2}` + }, [multiSelect, multiSelectValues, allOptions]) + /** * Filter options based on current value or search query */ @@ -590,11 +606,11 @@ const Combobox = memo( - {selectedOption ? selectedOption.label : placeholder} + {multiSelectLabel ?? (selectedOption ? selectedOption.label : placeholder)} Date: Fri, 22 May 2026 08:28:31 -0700 Subject: [PATCH 2/4] chore(kb-connectors): strip redundant field-level descriptions Removed 41 inline `description:` lines from configFields across 16 connectors (Slack, MS Teams, GCal, Gmail, Notion, Linear-adjacent, Discord, Dropbox, Evernote, Fireflies, Google Sheets, Intercom, Obsidian, Outlook, Reddit, ServiceNow, WordPress, Zendesk). They mostly restated the field title (e.g. "Channels to sync messages from" under a "Channels" label) and cluttered the add/edit modal. Field titles + placeholders already communicate intent. Connector-level `description` (used in the connector picker grid) is unchanged. --- apps/sim/connectors/discord/discord.ts | 1 - apps/sim/connectors/dropbox/dropbox.ts | 1 - apps/sim/connectors/evernote/evernote.ts | 1 - apps/sim/connectors/fireflies/fireflies.ts | 1 - apps/sim/connectors/gmail/gmail.ts | 3 --- apps/sim/connectors/google-calendar/google-calendar.ts | 2 -- apps/sim/connectors/google-sheets/google-sheets.ts | 1 - apps/sim/connectors/intercom/intercom.ts | 4 ---- apps/sim/connectors/microsoft-teams/microsoft-teams.ts | 2 -- apps/sim/connectors/obsidian/obsidian.ts | 2 -- apps/sim/connectors/outlook/outlook.ts | 1 - apps/sim/connectors/reddit/reddit.ts | 3 --- apps/sim/connectors/servicenow/servicenow.ts | 8 -------- apps/sim/connectors/slack/slack.ts | 2 -- apps/sim/connectors/wordpress/wordpress.ts | 3 --- apps/sim/connectors/zendesk/zendesk.ts | 6 ------ 16 files changed, 41 deletions(-) diff --git a/apps/sim/connectors/discord/discord.ts b/apps/sim/connectors/discord/discord.ts index 51679871692..0861bc0c01f 100644 --- a/apps/sim/connectors/discord/discord.ts +++ b/apps/sim/connectors/discord/discord.ts @@ -154,7 +154,6 @@ export const discordConnector: ConnectorConfig = { type: 'short-input', placeholder: 'e.g. 123456789012345678', required: true, - description: 'The Discord channel ID to sync messages from', }, { id: 'maxMessages', diff --git a/apps/sim/connectors/dropbox/dropbox.ts b/apps/sim/connectors/dropbox/dropbox.ts index c612eb5aca4..490fbf342df 100644 --- a/apps/sim/connectors/dropbox/dropbox.ts +++ b/apps/sim/connectors/dropbox/dropbox.ts @@ -115,7 +115,6 @@ export const dropboxConnector: ConnectorConfig = { type: 'short-input', placeholder: 'e.g. /Documents (default: entire Dropbox)', required: false, - description: 'Leave empty to sync all supported files', }, { id: 'maxFiles', diff --git a/apps/sim/connectors/evernote/evernote.ts b/apps/sim/connectors/evernote/evernote.ts index 2dfd2271f5f..bfe7b85aa17 100644 --- a/apps/sim/connectors/evernote/evernote.ts +++ b/apps/sim/connectors/evernote/evernote.ts @@ -383,7 +383,6 @@ export const evernoteConnector: ConnectorConfig = { type: 'short-input', placeholder: 'Leave empty to sync all notebooks', required: false, - description: 'Sync only notes from this notebook (optional)', }, ], diff --git a/apps/sim/connectors/fireflies/fireflies.ts b/apps/sim/connectors/fireflies/fireflies.ts index 0251fb4a697..3ebbbb23f1e 100644 --- a/apps/sim/connectors/fireflies/fireflies.ts +++ b/apps/sim/connectors/fireflies/fireflies.ts @@ -140,7 +140,6 @@ export const firefliesConnector: ConnectorConfig = { type: 'short-input', placeholder: 'e.g. john@example.com', required: false, - description: 'Only sync transcripts hosted by this email', }, { id: 'maxTranscripts', diff --git a/apps/sim/connectors/gmail/gmail.ts b/apps/sim/connectors/gmail/gmail.ts index eab77834c27..3b772ff7856 100644 --- a/apps/sim/connectors/gmail/gmail.ts +++ b/apps/sim/connectors/gmail/gmail.ts @@ -360,7 +360,6 @@ export const gmailConnector: ConnectorConfig = { multi: true, placeholder: 'Select one or more labels', required: false, - description: 'Only sync emails matching any of these labels. Leave empty for all mail.', }, { id: 'label', @@ -371,7 +370,6 @@ export const gmailConnector: ConnectorConfig = { multi: true, placeholder: 'e.g. INBOX, IMPORTANT (comma-separated; commas in label names not supported)', required: false, - description: 'Only sync emails matching any of these labels. Leave empty for all mail.', }, { id: 'dateRange', @@ -413,7 +411,6 @@ export const gmailConnector: ConnectorConfig = { type: 'short-input', placeholder: 'e.g. from:boss@company.com subject:report has:attachment', required: false, - description: 'Additional Gmail search filter. Uses the same syntax as the Gmail search bar.', }, { id: 'maxThreads', diff --git a/apps/sim/connectors/google-calendar/google-calendar.ts b/apps/sim/connectors/google-calendar/google-calendar.ts index 104743b535d..d48f3b80ce4 100644 --- a/apps/sim/connectors/google-calendar/google-calendar.ts +++ b/apps/sim/connectors/google-calendar/google-calendar.ts @@ -267,7 +267,6 @@ export const googleCalendarConnector: ConnectorConfig = { multi: true, placeholder: 'Select one or more calendars', required: false, - description: 'Calendars to sync from. Defaults to your primary calendar.', }, { id: 'calendarId', @@ -299,7 +298,6 @@ export const googleCalendarConnector: ConnectorConfig = { type: 'short-input', placeholder: 'e.g. standup, sprint review (optional)', required: false, - description: 'Filter events by text search across all fields.', }, { id: 'maxEvents', diff --git a/apps/sim/connectors/google-sheets/google-sheets.ts b/apps/sim/connectors/google-sheets/google-sheets.ts index 38ac6b175a2..da1339b00d6 100644 --- a/apps/sim/connectors/google-sheets/google-sheets.ts +++ b/apps/sim/connectors/google-sheets/google-sheets.ts @@ -216,7 +216,6 @@ export const googleSheetsConnector: ConnectorConfig = { type: 'short-input', placeholder: 'e.g. 1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgVE2upms', required: true, - description: 'The ID from the spreadsheet URL: docs.google.com/spreadsheets/d/{ID}/edit', }, { id: 'sheetFilter', diff --git a/apps/sim/connectors/intercom/intercom.ts b/apps/sim/connectors/intercom/intercom.ts index 7b7362dc3d7..69cf8471e5f 100644 --- a/apps/sim/connectors/intercom/intercom.ts +++ b/apps/sim/connectors/intercom/intercom.ts @@ -285,7 +285,6 @@ export const intercomConnector: ConnectorConfig = { title: 'Content Type', type: 'dropdown', required: true, - description: 'Choose what to sync from Intercom', options: [ { label: 'Articles Only', id: 'articles' }, { label: 'Conversations Only', id: 'conversations' }, @@ -297,7 +296,6 @@ export const intercomConnector: ConnectorConfig = { title: 'Article State', type: 'dropdown', required: false, - description: 'Filter articles by state (default: published)', options: [ { label: 'Published', id: 'published' }, { label: 'Draft', id: 'draft' }, @@ -309,7 +307,6 @@ export const intercomConnector: ConnectorConfig = { title: 'Conversation State', type: 'dropdown', required: false, - description: 'Filter conversations by state (default: all)', options: [ { label: 'Open', id: 'open' }, { label: 'Closed', id: 'closed' }, @@ -322,7 +319,6 @@ export const intercomConnector: ConnectorConfig = { type: 'short-input', required: false, placeholder: `e.g. 200 (default: ${DEFAULT_MAX_ITEMS})`, - description: 'Maximum number of articles or conversations to sync', }, ], diff --git a/apps/sim/connectors/microsoft-teams/microsoft-teams.ts b/apps/sim/connectors/microsoft-teams/microsoft-teams.ts index 3d18b127726..7c8f4a345e3 100644 --- a/apps/sim/connectors/microsoft-teams/microsoft-teams.ts +++ b/apps/sim/connectors/microsoft-teams/microsoft-teams.ts @@ -222,7 +222,6 @@ export const microsoftTeamsConnector: ConnectorConfig = { mode: 'advanced', placeholder: 'e.g. fbe2bf47-16c8-47cf-b4a5-4b9b187c508b', required: true, - description: 'The ID of the Microsoft Teams team', }, { id: 'channelSelector', @@ -245,7 +244,6 @@ export const microsoftTeamsConnector: ConnectorConfig = { multi: true, placeholder: 'e.g. General, Announcements (comma-separated for multiple)', required: true, - description: 'Channel names or IDs to sync messages from', }, { id: 'maxMessages', diff --git a/apps/sim/connectors/obsidian/obsidian.ts b/apps/sim/connectors/obsidian/obsidian.ts index 964aa33b573..734fb674e5a 100644 --- a/apps/sim/connectors/obsidian/obsidian.ts +++ b/apps/sim/connectors/obsidian/obsidian.ts @@ -183,7 +183,6 @@ export const obsidianConnector: ConnectorConfig = { type: 'short-input', placeholder: 'https://127.0.0.1:27124', required: true, - description: 'Base URL of your Obsidian Local REST API (default port: 27124 for HTTPS)', }, { id: 'folderPath', @@ -191,7 +190,6 @@ export const obsidianConnector: ConnectorConfig = { type: 'short-input', placeholder: 'e.g. Projects/Notes', required: false, - description: 'Only sync notes from this folder (leave empty for entire vault)', }, ], diff --git a/apps/sim/connectors/outlook/outlook.ts b/apps/sim/connectors/outlook/outlook.ts index 81d750b7f2c..d064819afab 100644 --- a/apps/sim/connectors/outlook/outlook.ts +++ b/apps/sim/connectors/outlook/outlook.ts @@ -334,7 +334,6 @@ export const outlookConnector: ConnectorConfig = { type: 'short-input', placeholder: 'e.g. from:boss@company.com subject:report hasAttachment:true', required: false, - description: 'Search filter using Outlook KQL syntax.', }, { id: 'maxConversations', diff --git a/apps/sim/connectors/reddit/reddit.ts b/apps/sim/connectors/reddit/reddit.ts index ccb5c53dbe9..77c7ff4d8e0 100644 --- a/apps/sim/connectors/reddit/reddit.ts +++ b/apps/sim/connectors/reddit/reddit.ts @@ -262,14 +262,12 @@ export const redditConnector: ConnectorConfig = { type: 'short-input', placeholder: 'e.g. machinelearning', required: true, - description: 'Subreddit name to sync posts from (without r/ prefix)', }, { id: 'sort', title: 'Sort', type: 'dropdown', required: false, - description: 'How to sort posts', options: [ { label: 'Hot', id: 'hot' }, { label: 'New', id: 'new' }, @@ -297,7 +295,6 @@ export const redditConnector: ConnectorConfig = { type: 'short-input', required: false, placeholder: `e.g. 100 (default: ${DEFAULT_MAX_POSTS})`, - description: 'Maximum number of posts to sync', }, ], diff --git a/apps/sim/connectors/servicenow/servicenow.ts b/apps/sim/connectors/servicenow/servicenow.ts index d8b921bddff..300895920cb 100644 --- a/apps/sim/connectors/servicenow/servicenow.ts +++ b/apps/sim/connectors/servicenow/servicenow.ts @@ -452,7 +452,6 @@ export const servicenowConnector: ConnectorConfig = { type: 'short-input', placeholder: 'yourinstance.service-now.com', required: true, - description: 'Your ServiceNow instance URL', }, { id: 'username', @@ -460,14 +459,12 @@ export const servicenowConnector: ConnectorConfig = { type: 'short-input', placeholder: 'admin', required: true, - description: 'ServiceNow username for Basic Auth', }, { id: 'contentType', title: 'Content Type', type: 'dropdown', required: true, - description: 'Type of content to sync from ServiceNow', options: [ { label: 'Knowledge Base Articles', id: 'kb_knowledge' }, { label: 'Incidents', id: 'incident' }, @@ -478,7 +475,6 @@ export const servicenowConnector: ConnectorConfig = { title: 'Article State', type: 'dropdown', required: false, - description: 'Filter KB articles by workflow state', options: [ { label: 'All States', id: 'all' }, { label: 'Published', id: 'published' }, @@ -494,14 +490,12 @@ export const servicenowConnector: ConnectorConfig = { type: 'short-input', required: false, placeholder: 'e.g. IT, HR, General', - description: 'Filter KB articles by category label', }, { id: 'incidentState', title: 'Incident State', type: 'dropdown', required: false, - description: 'Filter incidents by state', options: [ { label: 'All States', id: 'all' }, { label: 'New', id: '1' }, @@ -517,7 +511,6 @@ export const servicenowConnector: ConnectorConfig = { title: 'Incident Priority', type: 'dropdown', required: false, - description: 'Filter incidents by priority', options: [ { label: 'All Priorities', id: 'all' }, { label: 'Critical', id: '1' }, @@ -533,7 +526,6 @@ export const servicenowConnector: ConnectorConfig = { type: 'short-input', required: false, placeholder: `e.g. 200 (default: ${DEFAULT_MAX_ITEMS})`, - description: 'Maximum number of items to sync', }, ], diff --git a/apps/sim/connectors/slack/slack.ts b/apps/sim/connectors/slack/slack.ts index 814fe13a734..3ecbed68566 100644 --- a/apps/sim/connectors/slack/slack.ts +++ b/apps/sim/connectors/slack/slack.ts @@ -513,7 +513,6 @@ export const slackConnector: ConnectorConfig = { multi: true, placeholder: 'Select one or more channels', required: true, - description: 'Channels to sync messages from', }, { id: 'channel', @@ -524,7 +523,6 @@ export const slackConnector: ConnectorConfig = { multi: true, placeholder: 'e.g. general, C01ABC23DEF (comma-separated for multiple)', required: true, - description: 'Channel names or IDs to sync messages from', }, { id: 'maxMessages', diff --git a/apps/sim/connectors/wordpress/wordpress.ts b/apps/sim/connectors/wordpress/wordpress.ts index 2abe45c9264..b274379f0cd 100644 --- a/apps/sim/connectors/wordpress/wordpress.ts +++ b/apps/sim/connectors/wordpress/wordpress.ts @@ -115,14 +115,12 @@ export const wordpressConnector: ConnectorConfig = { type: 'short-input', placeholder: 'e.g. mysite.wordpress.com', required: true, - description: 'WordPress site domain', }, { id: 'postType', title: 'Post Type', type: 'dropdown', required: false, - description: 'Filter by content type', options: [ { label: 'Both', id: 'Both' }, { label: 'Posts', id: 'Posts' }, @@ -135,7 +133,6 @@ export const wordpressConnector: ConnectorConfig = { type: 'short-input', required: false, placeholder: `e.g. 50 (default: ${DEFAULT_MAX_POSTS})`, - description: 'Maximum number of posts to sync', }, ], diff --git a/apps/sim/connectors/zendesk/zendesk.ts b/apps/sim/connectors/zendesk/zendesk.ts index a71234d1747..8e0e03bd9a8 100644 --- a/apps/sim/connectors/zendesk/zendesk.ts +++ b/apps/sim/connectors/zendesk/zendesk.ts @@ -332,7 +332,6 @@ export const zendeskConnector: ConnectorConfig = { type: 'short-input', placeholder: 'yourcompany (from yourcompany.zendesk.com)', required: true, - description: 'Your Zendesk subdomain', }, { id: 'email', @@ -340,14 +339,12 @@ export const zendeskConnector: ConnectorConfig = { type: 'short-input', placeholder: 'agent@yourcompany.com', required: true, - description: 'Email address of the Zendesk user for API authentication', }, { id: 'contentType', title: 'Content Type', type: 'dropdown', required: true, - description: 'What content to sync from Zendesk', options: [ { label: 'Articles & Tickets', id: 'both' }, { label: 'Help Center Articles Only', id: 'articles' }, @@ -359,7 +356,6 @@ export const zendeskConnector: ConnectorConfig = { title: 'Ticket Status Filter', type: 'dropdown', required: false, - description: 'Filter tickets by status (applies only when syncing tickets)', options: [ { label: 'All Statuses', id: 'all' }, { label: 'New', id: 'new' }, @@ -376,7 +372,6 @@ export const zendeskConnector: ConnectorConfig = { type: 'short-input', required: false, placeholder: 'e.g. en-us (default: all locales)', - description: 'Locale for Help Center articles', }, { id: 'maxTickets', @@ -384,7 +379,6 @@ export const zendeskConnector: ConnectorConfig = { type: 'short-input', required: false, placeholder: `e.g. 200 (default: ${DEFAULT_MAX_TICKETS})`, - description: 'Maximum number of tickets to sync', }, ], From c81fabba78ed5395211070ca56ec933b81d68b1d Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 22 May 2026 08:35:28 -0700 Subject: [PATCH 3/4] test(leader-lock): use fake timers to deterministically test follower polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "follower does a final read after timeout" test (and the "follower returns null after timeout" test) relied on real-clock `setTimeout` and `Date.now()` with very tight bounds (pollIntervalMs=5, maxWaitMs=9). Any CI scheduler jitter of >4ms would cause the second in-loop poll to be skipped, the polls counter to end at 2 instead of 3, and the assertion `expect(result).toBe('late-leader')` to fail. Switched both tests to `vi.useFakeTimers()` so the schedule is driven by mocked time advanced via `vi.advanceTimersByTimeAsync`. The intent is unchanged — verify that the in-loop deadline triggers exactly one post-deadline last-chance call to `onFollower` — but the assertions no longer depend on wall-clock timing. Verified across 5 sequential runs with zero flakes. --- .../concurrency/__tests__/leader-lock.test.ts | 73 ++++++++++++------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/apps/sim/lib/concurrency/__tests__/leader-lock.test.ts b/apps/sim/lib/concurrency/__tests__/leader-lock.test.ts index 8b3e2a3a06b..bd0a11c548b 100644 --- a/apps/sim/lib/concurrency/__tests__/leader-lock.test.ts +++ b/apps/sim/lib/concurrency/__tests__/leader-lock.test.ts @@ -120,39 +120,62 @@ describe('withLeaderLock', () => { it('follower does a final read after timeout to catch a just-finished leader', async () => { redisConfigMockFns.mockAcquireLock.mockResolvedValueOnce(false) - // pollInterval=5, maxWait=9 → loop exits after 2 in-loop polls (T+5, T+10); - // the third call (polls=3) is the post-deadline last-chance read. - let polls = 0 - const onFollower = vi.fn(async () => { - polls += 1 - if (polls <= 2) return null - return 'late-leader' - }) + /** + * The intent: after the in-loop poll deadline is reached, the follower + * does exactly one more (last-chance) `onFollower` call to catch a leader + * that finished between the previous poll and the timeout. Using fake + * timers makes the timing deterministic — pollInterval=10 and maxWait=15 + * cause two in-loop polls (T+10, T+20) and one last-chance read (T+20), + * but the schedule is driven by mocked time, not the CI wall clock. + */ + vi.useFakeTimers() + try { + let polls = 0 + const onFollower = vi.fn(async () => { + polls += 1 + if (polls <= 2) return null + return 'late-leader' + }) - const result = await withLeaderLock({ - key: 'k', - pollIntervalMs: 5, - maxWaitMs: 9, - onLeader: async () => 'should-not-run', - onFollower, - }) + const promise = withLeaderLock({ + key: 'k', + pollIntervalMs: 10, + maxWaitMs: 15, + onLeader: async () => 'should-not-run', + onFollower, + }) + + await vi.advanceTimersByTimeAsync(30) + const result = await promise - expect(result).toBe('late-leader') - expect(onFollower).toHaveBeenCalledTimes(3) + expect(result).toBe('late-leader') + expect(onFollower).toHaveBeenCalledTimes(3) + } finally { + vi.useRealTimers() + } }) it('follower returns null after timeout', async () => { redisConfigMockFns.mockAcquireLock.mockResolvedValueOnce(false) - const result = await withLeaderLock({ - key: 'k', - pollIntervalMs: 5, - maxWaitMs: 20, - onLeader: async () => 'should-not-run', - onFollower: async () => null, - }) + vi.useFakeTimers() + try { + const onFollower = vi.fn(async () => null) + const promise = withLeaderLock({ + key: 'k', + pollIntervalMs: 10, + maxWaitMs: 25, + onLeader: async () => 'should-not-run', + onFollower, + }) + + await vi.advanceTimersByTimeAsync(50) + const result = await promise - expect(result).toBeNull() + expect(result).toBeNull() + } finally { + vi.useRealTimers() + } }) it('only one of N concurrent callers acquires the lock', async () => { From 37a34cf6d09af5a39aeaaf05f2bc57a5c53d5eb2 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 22 May 2026 08:42:27 -0700 Subject: [PATCH 4/4] improvement(kb-connectors): restore field descriptions as info-icon tooltips Restores the 41 field-level `description` lines stripped in fc644210d, but instead of rendering them as inline muted-text paragraphs they're shown via a small Info icon next to each field title. Hovering or focusing the icon reveals the description in the existing emcn Tooltip. Keeps the modal layout tight while preserving the per-field guidance. Used + + {field.description} + )} - + {hasCanonicalPair && canonicalId && ( @@ -372,9 +388,6 @@ export function AddConnectorModal({ )} - {field.description && ( -

{field.description}

- )} {field.type === 'selector' && field.selectorKey ? (
- +
+ + {field.description && ( + + + + + {field.description} + + )} +
{hasCanonicalPair && canonicalId && ( @@ -406,9 +422,6 @@ function SettingsTab({ )}
- {field.description && ( -

{field.description}

- )} {field.type === 'selector' && field.selectorKey ? (