From b2f76c422520509c53e8db917c31a352d757b3fa Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Thu, 23 Jul 2026 17:45:44 +0530 Subject: [PATCH 01/24] Add desktop connector catalog metadata --- apps/desktop/src/source-setup.test.ts | 64 ++++- apps/desktop/src/source-setup.ts | 364 +++++++++++++++++++++++++- 2 files changed, 424 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/source-setup.test.ts b/apps/desktop/src/source-setup.test.ts index c60cce51a..7fe664c64 100644 --- a/apps/desktop/src/source-setup.test.ts +++ b/apps/desktop/src/source-setup.test.ts @@ -1,8 +1,18 @@ import { describe, expect, it } from "vitest"; import { connectedSourcesReadyToMount, + isSourceCatalogConnectorId, isSourceConnectorId, + plannedSourceConnectorDefinitions, + plannedSourceConnectorIds, + sourceCatalogConnectorDefinition, + sourceConnectorCatalogDefinitions, sourceConnectorIds, + sourceConnectorDefaultMountDirectory, + sourceConnectorDefaultMountId, + sourceConnectorDefinition, + sourceConnectorDefinitions, + sourceConnectorName, sourceRequiresApiKey, sourceSkipsManualMountStep, sourceMounted, @@ -28,9 +38,16 @@ describe("source setup progress", () => { }); it("includes connector catalog entries with their auth modes", () => { - expect(sourceConnectorIds()).toContain("linear"); - expect(sourceConnectorIds()).toContain("slack"); - expect(sourceConnectorIds()).toContain("google-calendar"); + expect(sourceConnectorIds()).toEqual([ + "notion", + "google-docs", + "google-calendar", + "gmail", + "granola", + "linear", + "slack", + ]); + expect(sourceConnectorDefinitions().map((connector) => connector.id)).toEqual(sourceConnectorIds()); expect(sourceRequiresApiKey("linear")).toBe(true); expect(sourceRequiresApiKey("granola")).toBe(true); expect(sourceRequiresApiKey("slack")).toBe(false); @@ -39,6 +56,47 @@ describe("source setup progress", () => { expect(sourceSkipsManualMountStep("linear")).toBe(true); expect(sourceSkipsManualMountStep("slack")).toBe(true); expect(sourceSkipsManualMountStep("google-calendar")).toBe(true); + expect(sourceConnectorName("google-docs")).toBe("Google Docs"); + expect(sourceConnectorDefaultMountId("google-docs")).toBe("google-docs-main"); + expect(sourceConnectorDefaultMountDirectory("google-docs")).toBe("google-docs-main"); + expect(sourceConnectorDefinition("notion").availability).toBe("implemented"); + }); + + it("keeps planned connector catalog entries separate from runtime setup", () => { + expect(plannedSourceConnectorIds()).toEqual([ + "confluence", + "jira", + "sharepoint", + "onedrive", + "outlook-mail", + "outlook-calendar", + "microsoft-teams", + "github", + "gitlab", + "google-drive", + "dropbox", + "box", + "figma", + "asana", + "clickup", + "zendesk", + "intercom", + "hubspot", + "salesforce", + "fhir", + ]); + expect(sourceConnectorCatalogDefinitions()).toHaveLength(27); + expect(plannedSourceConnectorDefinitions()).toHaveLength(20); + expect(isSourceConnectorId("confluence")).toBe(false); + expect(isSourceCatalogConnectorId("confluence")).toBe(true); + expect(isSourceCatalogConnectorId("notion")).toBe(true); + expect(sourceCatalogConnectorDefinition("github").authModes).toEqual([ + "oauth", + "github-app", + "personal-token", + ]); + expect(sourceCatalogConnectorDefinition("fhir").authModes).toEqual(["smart-oauth"]); + expect(sourceCatalogConnectorDefinition("confluence").availability).toBe("planned"); }); it("keeps connected but unmounted sources visible when another source is mounted", () => { diff --git a/apps/desktop/src/source-setup.ts b/apps/desktop/src/source-setup.ts index abacfca02..053a77d73 100644 --- a/apps/desktop/src/source-setup.ts +++ b/apps/desktop/src/source-setup.ts @@ -2,8 +2,312 @@ import { classifyMountSetupError } from "./onboarding-errors"; export type SourceSetupState = "idle" | "connecting" | "creating" | "changing" | "success" | "error"; const SOURCE_CONNECTORS = ["notion", "google-docs", "google-calendar", "gmail", "granola", "linear", "slack"] as const; +const PLANNED_SOURCE_CONNECTORS = [ + "confluence", + "jira", + "sharepoint", + "onedrive", + "outlook-mail", + "outlook-calendar", + "microsoft-teams", + "github", + "gitlab", + "google-drive", + "dropbox", + "box", + "figma", + "asana", + "clickup", + "zendesk", + "intercom", + "hubspot", + "salesforce", + "fhir", +] as const; + export type SourceConnectorId = (typeof SOURCE_CONNECTORS)[number]; +export type PlannedSourceConnectorId = (typeof PLANNED_SOURCE_CONNECTORS)[number]; +export type SourceCatalogConnectorId = SourceConnectorId | PlannedSourceConnectorId; export type ApiKeySourceConnectorId = Extract; +export type SourceConnectorAvailability = "implemented" | "planned"; +export type SourceConnectorCategory = "knowledge" | "action" | "hybrid"; +export type SourceConnectorAuthMode = "oauth" | "api-key" | "api-token" | "personal-token" | "github-app" | "smart-oauth"; + +export type SourceConnectorDefinition = { + id: Id; + name: string; + description: string; + availability: SourceConnectorAvailability; + category: SourceConnectorCategory; + authModes: readonly SourceConnectorAuthMode[]; + keywords: readonly string[]; + defaultMountId?: string; + defaultMountDirectory?: string; +}; + +const SOURCE_CONNECTOR_DEFINITIONS: readonly SourceConnectorDefinition[] = [ + { + id: "notion", + name: "Notion", + description: "Pages and databases as folders with page.md files.", + availability: "implemented", + category: "knowledge", + authModes: ["oauth"], + keywords: ["notion", "wiki", "pages", "database", "docs"], + defaultMountId: "notion-main", + defaultMountDirectory: "notion", + }, + { + id: "google-docs", + name: "Google Docs", + description: "Docs and Drive folders through the same local file workflow.", + availability: "implemented", + category: "knowledge", + authModes: ["oauth"], + keywords: ["google", "docs", "gdocs", "drive", "documents"], + defaultMountId: "google-docs-main", + defaultMountDirectory: "google-docs-main", + }, + { + id: "google-calendar", + name: "Google Calendar", + description: "Primary calendar events as files, new events from reviewed drafts.", + availability: "implemented", + category: "action", + authModes: ["oauth"], + keywords: ["google", "calendar", "gcal", "events", "meet"], + defaultMountId: "google-calendar-main", + defaultMountDirectory: "google-calendar-main", + }, + { + id: "gmail", + name: "Gmail", + description: "Inbox and sent as readable files, drafts as reviewed outbound mail.", + availability: "implemented", + category: "action", + authModes: ["oauth"], + keywords: ["gmail", "mail", "email", "inbox", "drafts"], + defaultMountId: "gmail-main", + defaultMountDirectory: "gmail-main", + }, + { + id: "granola", + name: "Granola", + description: "Meeting summaries and raw transcripts as read-only files.", + availability: "implemented", + category: "knowledge", + authModes: ["api-key"], + keywords: ["granola", "meetings", "notes", "transcripts", "summaries"], + defaultMountId: "granola-main", + defaultMountDirectory: "granola", + }, + { + id: "linear", + name: "Linear", + description: "Issues and teams as editable Markdown files.", + availability: "implemented", + category: "hybrid", + authModes: ["api-key"], + keywords: ["linear", "issues", "tickets", "projects", "teams"], + defaultMountId: "linear-main", + defaultMountDirectory: "linear", + }, + { + id: "slack", + name: "Slack", + description: "Recent accessible conversations as read-only Markdown.", + availability: "implemented", + category: "knowledge", + authModes: ["oauth"], + keywords: ["slack", "channels", "private channels", "dms", "group dms", "users"], + defaultMountId: "slack-main", + defaultMountDirectory: "slack", + }, +]; + +const PLANNED_SOURCE_CONNECTOR_DEFINITIONS: readonly SourceConnectorDefinition[] = [ + { + id: "confluence", + name: "Confluence", + description: "Spaces, pages, and knowledge-base articles as local documents.", + availability: "planned", + category: "knowledge", + authModes: ["oauth", "api-token"], + keywords: ["atlassian", "confluence", "wiki", "spaces", "pages"], + }, + { + id: "jira", + name: "Jira", + description: "Projects, issues, comments, and sprint context for planning workflows.", + availability: "planned", + category: "hybrid", + authModes: ["oauth", "api-token"], + keywords: ["atlassian", "jira", "issues", "sprints", "projects"], + }, + { + id: "sharepoint", + name: "SharePoint", + description: "Team sites, libraries, pages, and enterprise documents.", + availability: "planned", + category: "knowledge", + authModes: ["oauth"], + keywords: ["microsoft", "sharepoint", "sites", "libraries", "documents"], + }, + { + id: "onedrive", + name: "OneDrive", + description: "User and team files projected into agent-friendly folders.", + availability: "planned", + category: "knowledge", + authModes: ["oauth"], + keywords: ["microsoft", "onedrive", "files", "drive", "documents"], + }, + { + id: "outlook-mail", + name: "Outlook Mail", + description: "Mailbox context with reviewed draft creation for outbound updates.", + availability: "planned", + category: "action", + authModes: ["oauth"], + keywords: ["microsoft", "outlook", "mail", "email", "drafts"], + }, + { + id: "outlook-calendar", + name: "Outlook Calendar", + description: "Calendar events and scheduling drafts for Microsoft 365 teams.", + availability: "planned", + category: "action", + authModes: ["oauth"], + keywords: ["microsoft", "outlook", "calendar", "events", "meetings"], + }, + { + id: "microsoft-teams", + name: "Microsoft Teams", + description: "Channels, chats, meetings, and team knowledge for enterprise collaboration.", + availability: "planned", + category: "knowledge", + authModes: ["oauth"], + keywords: ["microsoft", "teams", "channels", "chats", "meetings"], + }, + { + id: "github", + name: "GitHub", + description: "Repositories, pull requests, issues, discussions, and review activity.", + availability: "planned", + category: "hybrid", + authModes: ["oauth", "github-app", "personal-token"], + keywords: ["github", "git", "repos", "pull requests", "issues"], + }, + { + id: "gitlab", + name: "GitLab", + description: "Projects, merge requests, issues, pipelines, and release context.", + availability: "planned", + category: "hybrid", + authModes: ["oauth", "personal-token"], + keywords: ["gitlab", "git", "repos", "merge requests", "pipelines"], + }, + { + id: "google-drive", + name: "Google Drive", + description: "Drive files, shared folders, PDFs, sheets, and presentations.", + availability: "planned", + category: "knowledge", + authModes: ["oauth"], + keywords: ["google", "drive", "files", "shared drives", "docs"], + }, + { + id: "dropbox", + name: "Dropbox", + description: "Shared team files and folder hierarchies for document workflows.", + availability: "planned", + category: "knowledge", + authModes: ["oauth"], + keywords: ["dropbox", "files", "folders", "documents", "team"], + }, + { + id: "box", + name: "Box", + description: "Enterprise file libraries with permission-aware local projection.", + availability: "planned", + category: "knowledge", + authModes: ["oauth"], + keywords: ["box", "files", "folders", "enterprise", "documents"], + }, + { + id: "figma", + name: "Figma", + description: "Design files, comments, components, and product design context.", + availability: "planned", + category: "knowledge", + authModes: ["oauth", "personal-token"], + keywords: ["figma", "design", "comments", "components", "files"], + }, + { + id: "asana", + name: "Asana", + description: "Projects, tasks, comments, and status updates for team workflows.", + availability: "planned", + category: "hybrid", + authModes: ["oauth", "personal-token"], + keywords: ["asana", "tasks", "projects", "status", "workflows"], + }, + { + id: "clickup", + name: "ClickUp", + description: "Spaces, lists, tasks, docs, and comments for operating teams.", + availability: "planned", + category: "hybrid", + authModes: ["oauth", "api-token"], + keywords: ["clickup", "tasks", "docs", "lists", "spaces"], + }, + { + id: "zendesk", + name: "Zendesk", + description: "Tickets, help-center articles, macros, and support context.", + availability: "planned", + category: "hybrid", + authModes: ["oauth", "api-token"], + keywords: ["zendesk", "support", "tickets", "help center", "macros"], + }, + { + id: "intercom", + name: "Intercom", + description: "Conversations, help articles, contacts, and support workflows.", + availability: "planned", + category: "hybrid", + authModes: ["oauth"], + keywords: ["intercom", "support", "conversations", "help center", "contacts"], + }, + { + id: "hubspot", + name: "HubSpot", + description: "CRM records, notes, tasks, emails, deals, and customer context.", + availability: "planned", + category: "hybrid", + authModes: ["oauth", "api-token"], + keywords: ["hubspot", "crm", "contacts", "deals", "tasks"], + }, + { + id: "salesforce", + name: "Salesforce", + description: "Accounts, opportunities, cases, notes, tasks, and CRM knowledge.", + availability: "planned", + category: "hybrid", + authModes: ["oauth"], + keywords: ["salesforce", "crm", "accounts", "opportunities", "cases"], + }, + { + id: "fhir", + name: "FHIR", + description: "Healthcare records through SMART on FHIR profiles and scoped access.", + availability: "planned", + category: "knowledge", + authModes: ["smart-oauth"], + keywords: ["fhir", "smart", "healthcare", "clinical", "ehr"], + }, +]; + export type SourceMountRetryOutcome = | { kind: "retry" } | { kind: "success" | "error"; message: string }; @@ -29,8 +333,62 @@ export function sourceConnectorIds(): SourceConnectorId[] { return [...SOURCE_CONNECTORS]; } +export function plannedSourceConnectorIds(): PlannedSourceConnectorId[] { + return [...PLANNED_SOURCE_CONNECTORS]; +} + +export function sourceConnectorDefinitions(): SourceConnectorDefinition[] { + return SOURCE_CONNECTOR_DEFINITIONS.map((definition) => ({ ...definition })); +} + +export function plannedSourceConnectorDefinitions(): SourceConnectorDefinition[] { + return PLANNED_SOURCE_CONNECTOR_DEFINITIONS.map((definition) => ({ ...definition })); +} + +export function sourceConnectorCatalogDefinitions(): SourceConnectorDefinition[] { + return [ + ...sourceConnectorDefinitions(), + ...plannedSourceConnectorDefinitions(), + ]; +} + +export function sourceConnectorCatalogIds(): SourceCatalogConnectorId[] { + return [ + ...SOURCE_CONNECTORS, + ...PLANNED_SOURCE_CONNECTORS, + ]; +} + +export function sourceConnectorDefinition(connector: SourceConnectorId): SourceConnectorDefinition { + const definition = SOURCE_CONNECTOR_DEFINITIONS.find((candidate) => candidate.id === connector); + if (!definition) { + throw new Error(`Unknown source connector: ${connector}`); + } + return { ...definition }; +} + +export function sourceCatalogConnectorDefinition(connector: SourceCatalogConnectorId): SourceConnectorDefinition { + const definition = sourceConnectorCatalogDefinitions().find((candidate) => candidate.id === connector); + if (!definition) { + throw new Error(`Unknown source connector: ${connector}`); + } + return definition; +} + +export function sourceConnectorName(connector: SourceConnectorId): string { + return sourceConnectorDefinition(connector).name; +} + +export function sourceConnectorDefaultMountId(connector: SourceConnectorId): string { + return sourceConnectorDefinition(connector).defaultMountId ?? `${connector}-main`; +} + +export function sourceConnectorDefaultMountDirectory(connector: SourceConnectorId): string { + return sourceConnectorDefinition(connector).defaultMountDirectory ?? sourceConnectorDefaultMountId(connector); +} + export function sourceRequiresApiKey(connector: SourceConnectorId): connector is ApiKeySourceConnectorId { - return connector === "granola" || connector === "linear"; + return sourceConnectorDefinition(connector).authModes.includes("api-key"); } export function sourceSkipsManualMountStep(connector: SourceConnectorId): boolean { @@ -81,6 +439,10 @@ export function isSourceConnectorId(value: string): value is SourceConnectorId { return SOURCE_CONNECTORS.includes(value as SourceConnectorId); } +export function isSourceCatalogConnectorId(value: string): value is SourceCatalogConnectorId { + return sourceConnectorCatalogIds().includes(value as SourceCatalogConnectorId); +} + export function sourceConnectionReady( snapshot: SourceSnapshotLike, connector: SourceConnectorId, From 45719e1370373d2964103e3ea79aea491b9d3116 Mon Sep 17 00:00:00 2001 From: Sarthak Agarwal Date: Thu, 23 Jul 2026 17:48:22 +0530 Subject: [PATCH 02/24] Surface planned connectors in add source --- apps/desktop/src/App.tsx | 259 +++++++++++++++++------------------- apps/desktop/src/styles.css | 21 +++ 2 files changed, 140 insertions(+), 140 deletions(-) diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 067b36cdf..829f75e16 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -23,6 +23,7 @@ import { Minus, Monitor, Moon, + Plug, Plus, Power, RefreshCw, @@ -98,8 +99,12 @@ import { import { connectedSourcesReadyToMount, isSourceConnectorId, + sourceConnectorCatalogDefinitions, + sourceConnectorDefaultMountDirectory as sourceConnectorDefaultMountDirectoryForId, + sourceConnectorDefaultMountId, sourceConnectionReady, sourceConnectorIds, + sourceConnectorName, sourceMountRetryOutcome, sourceMounted, sourceRequiresApiKey, @@ -107,7 +112,11 @@ import { sourceSetupIsActiveConnector, sourceSetupIsBusy, sourceSetupProgressLabel, + type SourceCatalogConnectorId, type SourceConnectorId, + type SourceConnectorAuthMode, + type SourceConnectorCategory, + type SourceConnectorAvailability, type SourceSetupState, } from "./source-setup"; import gmailIconUrl from "./assets/connectors/gmail.svg"; @@ -138,11 +147,14 @@ type AppTheme = "system" | "light" | "dark"; const APP_THEME_STORAGE_KEY = "locality.desktop.theme"; type ConnectorOption = { - id: SourceConnectorId; + id: SourceCatalogConnectorId; name: string; description: string; status: string; keywords: string[]; + availability: SourceConnectorAvailability; + category: SourceConnectorCategory; + authModes: readonly SourceConnectorAuthMode[]; mounted: boolean; }; @@ -3881,64 +3893,20 @@ function AddSourceDialog({ const [granolaApiKey, setGranolaApiKey] = useState(""); const [linearApiKey, setLinearApiKey] = useState(""); const busy = sourceSetupIsBusy(state); - const connectors: ConnectorOption[] = [ - { - id: "notion", - name: "Notion", - description: "Pages and databases as folders with page.md files.", - status: sourceConnectorStatus(snapshot, "notion"), - keywords: ["notion", "wiki", "pages", "database", "docs"], - mounted: sourceMounted(snapshot, "notion"), - }, - { - id: "google-docs", - name: "Google Docs", - description: "Docs and Drive folders through the same local file workflow.", - status: sourceConnectorStatus(snapshot, "google-docs"), - keywords: ["google", "docs", "gdocs", "drive", "documents"], - mounted: sourceMounted(snapshot, "google-docs"), - }, - { - id: "google-calendar", - name: "Google Calendar", - description: "Primary calendar events as files, new events from reviewed drafts.", - status: sourceConnectorStatus(snapshot, "google-calendar"), - keywords: ["google", "calendar", "gcal", "events", "meet"], - mounted: sourceMounted(snapshot, "google-calendar"), - }, - { - id: "gmail", - name: "Gmail", - description: "Inbox and sent as readable files, drafts as reviewed outbound mail.", - status: sourceConnectorStatus(snapshot, "gmail"), - keywords: ["gmail", "mail", "email", "inbox", "drafts"], - mounted: sourceMounted(snapshot, "gmail"), - }, - { - id: "granola", - name: "Granola", - description: "Meeting summaries and raw transcripts as read-only files.", - status: sourceConnectorStatus(snapshot, "granola"), - keywords: ["granola", "meetings", "notes", "transcripts", "summaries"], - mounted: sourceMounted(snapshot, "granola"), - }, - { - id: "linear", - name: "Linear", - description: "Issues and teams as editable Markdown files.", - status: sourceConnectorStatus(snapshot, "linear"), - keywords: ["linear", "issues", "tickets", "projects", "teams"], - mounted: sourceMounted(snapshot, "linear"), - }, - { - id: "slack", - name: "Slack", - description: "Recent accessible conversations as read-only Markdown.", - status: sourceConnectorStatus(snapshot, "slack"), - keywords: ["slack", "channels", "private channels", "dms", "group dms", "users"], - mounted: sourceMounted(snapshot, "slack"), - }, - ]; + const connectors: ConnectorOption[] = sourceConnectorCatalogDefinitions().map((definition) => { + const runtimeConnector = isSourceConnectorId(definition.id) ? definition.id : null; + return { + id: definition.id, + name: definition.name, + description: definition.description, + status: runtimeConnector ? sourceConnectorStatus(snapshot, runtimeConnector) : "Planned", + keywords: [...definition.keywords], + availability: definition.availability, + category: definition.category, + authModes: definition.authModes, + mounted: runtimeConnector ? sourceMounted(snapshot, runtimeConnector) : false, + }; + }); const normalizedQuery = query.trim().toLowerCase(); const visibleConnectors = normalizedQuery ? connectors.filter((connector) => @@ -4022,25 +3990,30 @@ function AddSourceDialog({
{visibleConnectors.map((connector) => { - const connectorBusy = sourceSetupIsActiveConnector(state, activeConnector, connector.id); - const apiKeyConnector = sourceRequiresApiKey(connector.id) ? connector.id : null; - const connected = sourceConnectionReady(snapshot, connector.id); + const runtimeConnector = isSourceConnectorId(connector.id) ? connector.id : null; + const connectorBusy = runtimeConnector + ? sourceSetupIsActiveConnector(state, activeConnector, runtimeConnector) + : false; + const apiKeyConnector = runtimeConnector && sourceRequiresApiKey(runtimeConnector) ? runtimeConnector : null; + const connected = runtimeConnector ? sourceConnectionReady(snapshot, runtimeConnector) : false; const needsConnection = !connected; const needsFolder = connected && !connector.mounted; - const connectionDetails = sourceConnectionDetails(snapshot, connector.id); - const mountDetails = sourceMountDetails(snapshot, connector.id); - const disabled = busy || (connector.id !== "notion" && connector.mounted); + const connectionDetails = runtimeConnector ? sourceConnectionDetails(snapshot, runtimeConnector) : null; + const mountDetails = runtimeConnector ? sourceMountDetails(snapshot, runtimeConnector) : null; + const disabled = !runtimeConnector || busy || (runtimeConnector !== "notion" && connector.mounted); const displayedStatus = connectorBusy ? sourceSetupProgressLabel(state, connector.mounted) : connector.status; - const actionLabel = sourceActionLabel(connector.id, { - needsConnection, - needsFolder, - mounted: connector.mounted, - }); + const actionLabel = runtimeConnector + ? sourceActionLabel(runtimeConnector, { + needsConnection, + needsFolder, + mounted: connector.mounted, + }) + : "Planned"; return (
@@ -4054,7 +4027,9 @@ function AddSourceDialog({ tone={ connectorBusy ? "warn" - : connector.mounted || (connector.id === "notion" && !needsConnection && !needsFolder) + : !runtimeConnector + ? "warn" + : connector.mounted || (runtimeConnector === "notion" && !needsConnection && !needsFolder) ? "ready" : "warn" } @@ -4064,46 +4039,52 @@ function AddSourceDialog({
- {connector.id === "notion" ? ( + {!runtimeConnector ? ( + <> + + + + + ) : runtimeConnector === "notion" ? ( <> - + - ) : connector.id === "google-docs" ? ( + ) : runtimeConnector === "google-docs" ? ( <> - + - ) : connector.id === "google-calendar" ? ( + ) : runtimeConnector === "google-calendar" ? ( <> - + - ) : connector.id === "granola" ? ( + ) : runtimeConnector === "granola" ? ( <> - + - ) : connector.id === "linear" ? ( + ) : runtimeConnector === "linear" ? ( <> - + - ) : connector.id === "slack" ? ( + ) : runtimeConnector === "slack" ? ( <> - + ) : ( <> - + )}
- {connector.id === "google-docs" && !connector.mounted && ( + {runtimeConnector === "google-docs" && !connector.mounted && (