From 78f5319a1314839c36092e7906224d8f58caf37b Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Thu, 1 Oct 2026 12:47:49 +0530 Subject: [PATCH 1/4] feat: support direct provider authentication and identity hooks --- electron.vite.config.ts | 21 ++++++++++++++++++++- src/main/mcp-ipc.ts | 2 ++ src/main/mcp-oauth-loopback.ts | 3 ++- src/main/mcp-oauth.ts | 14 +++++++++----- src/main/mcp.ts | 31 +++++++++++++++++++++++++++---- src/preload/index.ts | 5 ++++- 6 files changed, 64 insertions(+), 12 deletions(-) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 6b8367a36..2f14ccfb1 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -2,6 +2,7 @@ import { resolve } from 'node:path' import { existsSync, readFileSync } from 'node:fs' import { randomBytes } from 'node:crypto' import { defineConfig, externalizeDepsPlugin } from 'electron-vite' +import { loadEnv } from 'vite' import react from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' import { createRendererContentSecurityPolicy } from './src/shared/renderer-csp' @@ -22,6 +23,19 @@ const proRenderer = proExists ? resolve('pro/renderer/index.tsx') : stub // Baked into every bundle so runtime code can tell a pro build from a free build // without relying on an env var default (which can't distinguish "unset" from "pro"). const proDefine = { __OFFGRID_PRO__: JSON.stringify(proExists) } +// Explicitly select a Desktop registration. A Web application's confidential secret must not +// enter an installed app. Google's Desktop client credential is a public application identity. +const microsoftEnv = loadEnv('production', process.cwd(), 'MICROSOFT_') +const microsoftClientId = process.env.MICROSOFT_CLIENT_ID || microsoftEnv.MICROSOFT_CLIENT_ID || '' +const googleEnv = loadEnv('production', process.cwd(), 'GOOGLE_') +const googleDesktopClient = + (process.env.GOOGLE_OAUTH_CLIENT_TYPE || googleEnv.GOOGLE_OAUTH_CLIENT_TYPE) === 'desktop' +const googleClientId = googleDesktopClient + ? process.env.GOOGLE_CLIENT_ID || googleEnv.GOOGLE_CLIENT_ID || '' + : '' +const googleClientSecret = googleDesktopClient + ? process.env.GOOGLE_CLIENT_SECRET || googleEnv.GOOGLE_CLIENT_SECRET || '' + : '' // Sourcemaps, for one purpose: making the e2e run's coverage land on source. // @@ -38,7 +52,12 @@ const rendererContentSecurityPolicy = createRendererContentSecurityPolicy(render export default defineConfig({ main: { - define: proDefine, + define: { + ...proDefine, + __OFFGRID_MICROSOFT_CLIENT_ID__: JSON.stringify(microsoftClientId), + __OFFGRID_GOOGLE_CLIENT_ID__: JSON.stringify(googleClientId), + __OFFGRID_GOOGLE_CLIENT_SECRET__: JSON.stringify(googleClientSecret) + }, build: { sourcemap: coverageSourcemap, // The embedding worker is a SECOND main-process entry, bundled beside index.js so diff --git a/src/main/mcp-ipc.ts b/src/main/mcp-ipc.ts index 9d7b47aa3..259ee3415 100644 --- a/src/main/mcp-ipc.ts +++ b/src/main/mcp-ipc.ts @@ -6,6 +6,7 @@ import { addConnector, setConnectorEnabled, removeConnector, + cancelConnectorAuthorization, testConnector, callConnectorTool, type NewConnector @@ -20,6 +21,7 @@ export function setupMcpIpc(): void { setConnectorEnabled(id, enabled) ) ipcMain.handle('mcp:remove', (_e, id: number) => removeConnector(id)) + ipcMain.handle('mcp:cancel', (_e, id: number) => cancelConnectorAuthorization(id)) ipcMain.handle('mcp:test', (_e, id: number) => testConnector(id)) ipcMain.handle('mcp:call', (_e, id: number, tool: string, args: unknown) => callConnectorTool(id, tool, args) diff --git a/src/main/mcp-oauth-loopback.ts b/src/main/mcp-oauth-loopback.ts index b1a1d715b..52b67e172 100644 --- a/src/main/mcp-oauth-loopback.ts +++ b/src/main/mcp-oauth-loopback.ts @@ -31,7 +31,8 @@ export class OAuthLoopbackServer { constructor(private readonly options: OAuthLoopbackOptions) { this.host = options.host ?? '127.0.0.1' - this.authorizationTimeoutMs = options.authorizationTimeoutMs ?? 3 * 60 * 1000 + // Account selection, MFA, and workspace approval can take several minutes. + this.authorizationTimeoutMs = options.authorizationTimeoutMs ?? 10 * 60 * 1000 } get redirectUrl(): string { diff --git a/src/main/mcp-oauth.ts b/src/main/mcp-oauth.ts index 159561ce4..8046921c0 100644 --- a/src/main/mcp-oauth.ts +++ b/src/main/mcp-oauth.ts @@ -43,7 +43,7 @@ function logoBytes(): Buffer | null { // least-privilege read scope + offline access (to get a refresh token). export interface StaticOAuthClient { client_id: string - client_secret: string + client_secret?: string scope: string } @@ -84,7 +84,7 @@ export function makeOAuthProvider( grant_types: ['authorization_code', 'refresh_token'], response_types: ['code'], // Google Desktop clients send the secret on token exchange; DCR uses none. - token_endpoint_auth_method: google ? 'client_secret_post' : 'none', + token_endpoint_auth_method: google?.client_secret ? 'client_secret_post' : 'none', ...(google ? { scope: google.scope } : {}) } }, @@ -93,7 +93,11 @@ export function makeOAuthProvider( }, clientInformation() { // Static (Google) client → skip DCR; otherwise use the registered one. - if (google) return { client_id: google.client_id, client_secret: google.client_secret } + if (google) + return { + client_id: google.client_id, + ...(google.client_secret ? { client_secret: google.client_secret } : {}) + } return loadJson('client') }, saveClientInformation(info: unknown): void { @@ -123,12 +127,12 @@ export function makeOAuthProvider( // saved token is stale, the sync just fails quietly and the user can // reconnect from the UI. Only interactive connects open the browser. if (!interactive) return - if (google) { + if (google && url.hostname === 'accounts.google.com') { // Google needs these for a refresh token, and we pin the scope so we // only ever request the least-privilege read scope (not every scope the // MCP server advertises, which would exceed the consent screen). url.searchParams.set('access_type', 'offline') - url.searchParams.set('prompt', 'consent') + url.searchParams.set('prompt', 'consent select_account') url.searchParams.set('scope', google.scope) } // Register for the redirect (keyed by state) BEFORE opening the browser, so diff --git a/src/main/mcp.ts b/src/main/mcp.ts index b128b959b..96a2a281c 100644 --- a/src/main/mcp.ts +++ b/src/main/mcp.ts @@ -6,7 +6,7 @@ // closed — we don't hold long-lived child processes. import { getDB } from './database' -import { deleteSecretsByPrefix, getSecret } from './secrets' +import { deleteSecretsByPrefix, getSecret, setSecret } from './secrets' import { makeOAuthProvider, ensureLoopback, hasOAuthTokens } from './mcp-oauth' import { cancelOAuthAuthorization } from './mcp-oauth-cancellation' import { callHook, HOOKS } from './bootstrap/hookRegistry' @@ -42,6 +42,8 @@ export interface ConnectorToolCallResult { */ export interface ConnectorToolSource { tools: ConnectorToolDefinition[] + /** Optional direct authorization owned by the provider, without a remote MCP handshake. */ + authorize?: () => Promise verify: () => Promise callTool: (tool: string, args: unknown) => Promise } @@ -90,6 +92,8 @@ export interface NewConnector { args?: string[] envKeys?: string[] // names of secrets to inject as env vars url?: string + /** Connect for live tools without background memory imports. */ + liveOnly?: boolean } export function listConnectors(): Connector[] { @@ -113,7 +117,13 @@ export function addConnector(c: NewConnector): number { c.url ?? null, Date.now() ) - return Number(info.lastInsertRowid) + const id = Number(info.lastInsertRowid) + if (c.liveOnly && !setSecret(`connector:${id}:live-only`, 'true')) { + getDB().prepare('DELETE FROM connectors WHERE id = ?').run(id) + throw new Error('Could not protect the connection settings.') + } + if (c.liveOnly) setConnectorEnabled(id, false) + return id } export function setConnectorEnabled(id: number, enabled: boolean): void { @@ -148,6 +158,10 @@ export function removeConnector(id: number): void { })() } +export function cancelConnectorAuthorization(id: number): void { + cancelOAuthAuthorization(id) +} + function getConnector(id: number): Connector | undefined { ensure() return getDB().prepare('SELECT * FROM connectors WHERE id = ?').get(id) as Connector | undefined @@ -298,8 +312,11 @@ export async function testConnector( // A fresh account still needs the existing interactive OAuth handshake. Once tokens exist, // provider verification must not touch a preview-gated MCP endpoint. if (!hasOAuthTokens(c.id)) { - const session = await connect(c, true) - await session.close() + if (source.authorize) await source.authorize() + else { + const session = await connect(c, true) + await session.close() + } } await source.verify() tools = source.tools @@ -307,6 +324,12 @@ export async function testConnector( const { client, close } = await connect(c, true) // user-initiated → allow browser OAuth try { const res = await client.listTools() + await callHook( + 'mcp:identifyAccount', + id, + client, + res.tools.map((tool) => tool.name) + ) tools = res.tools.map((tool) => ({ name: tool.name, description: tool.description diff --git a/src/preload/index.ts b/src/preload/index.ts index f872220f0..5f42619d8 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -574,7 +574,8 @@ const offGridApi = { pause: (): Promise => ipcRenderer.invoke('performance-pack:pause'), restart: (): Promise => ipcRenderer.invoke('performance-pack:restart'), onChanged: (callback: (status: PerformancePackStatus) => void): (() => void) => { - const subscription = (_event: unknown, status: PerformancePackStatus): void => callback(status) + const subscription = (_event: unknown, status: PerformancePackStatus): void => + callback(status) ipcRenderer.on('performance-pack:changed', subscription) return unsubscribe('performance-pack:changed', subscription) } @@ -950,10 +951,12 @@ const offGridApi = { args?: string[] envKeys?: string[] url?: string + liveOnly?: boolean }) => ipcRenderer.invoke('mcp:add', c), mcpSetEnabled: (id: number, enabled: boolean) => ipcRenderer.invoke('mcp:set-enabled', id, enabled), mcpRemove: (id: number) => ipcRenderer.invoke('mcp:remove', id), + mcpCancel: (id: number) => ipcRenderer.invoke('mcp:cancel', id), mcpTest: (id: number) => ipcRenderer.invoke('mcp:test', id), mcpIngest: (id: number, query?: string) => ipcRenderer.invoke('mcp:ingest', id, query), mcpItems: (surface: string) => ipcRenderer.invoke('mcp:items', surface), From 3b38993e5e4e2ec777645fcaf469f64756535a07 Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Thu, 1 Oct 2026 12:47:49 +0530 Subject: [PATCH 2/4] feat: add branded connection setup to onboarding and integrations --- package-lock.json | 23 ++ package.json | 3 +- src/renderer/src/App.tsx | 7 +- src/renderer/src/assets/logos/README.md | 10 + src/renderer/src/assets/logos/confluence.svg | 1 + src/renderer/src/assets/logos/google.svg | 1 + src/renderer/src/assets/logos/jira.svg | 1 + src/renderer/src/assets/logos/linear.svg | 1 + src/renderer/src/assets/logos/microsoft.svg | 1 + src/renderer/src/assets/logos/notion.svg | 1 + src/renderer/src/assets/logos/obsidian.svg | 1 + src/renderer/src/assets/main.css | 41 +++ src/renderer/src/bootstrap/slotRegistry.ts | 2 + .../src/components/ConnectorsScreen.tsx | 36 ++- src/renderer/src/components/Onboarding.tsx | 125 ++++++--- .../src/components/PermissionGate.tsx | 3 +- .../src/components/QuickConnections.tsx | 244 ++++++++++++++++++ ...oarding.model-control.integration.test.tsx | 2 + .../__tests__/QuickConnections.test.tsx | 96 +++++++ .../src/components/connectorCatalog.ts | 4 +- .../src/components/quickConnectionCatalog.ts | 1 + .../src/components/useQuickConnection.ts | 153 +++++++++++ src/renderer/src/lib/app-location.ts | 10 + src/renderer/src/theme.ts | 2 +- 24 files changed, 718 insertions(+), 51 deletions(-) create mode 100644 src/renderer/src/assets/logos/README.md create mode 100644 src/renderer/src/assets/logos/confluence.svg create mode 100644 src/renderer/src/assets/logos/google.svg create mode 100644 src/renderer/src/assets/logos/jira.svg create mode 100644 src/renderer/src/assets/logos/linear.svg create mode 100644 src/renderer/src/assets/logos/microsoft.svg create mode 100644 src/renderer/src/assets/logos/notion.svg create mode 100644 src/renderer/src/assets/logos/obsidian.svg create mode 100644 src/renderer/src/components/QuickConnections.tsx create mode 100644 src/renderer/src/components/__tests__/QuickConnections.test.tsx create mode 100644 src/renderer/src/components/quickConnectionCatalog.ts create mode 100644 src/renderer/src/components/useQuickConnection.ts create mode 100644 src/renderer/src/lib/app-location.ts diff --git a/package-lock.json b/package-lock.json index eb22b2d60..8995e987c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@offgrid/clipboard": "file:./packages/clipboard", "@offgrid/design": "file:./packages/design", "@offgrid/models": "file:../shared/packages/models", + "@offgrid/operator-ui": "github:wednesday-solutions/component-library-animations#ca35d2f9004b212ea034b73109fcd2819a08816b", "@offgrid/rag": "file:./packages/rag", "@offgrid/speech": "file:../shared/packages/speech", "@offgrid/sync": "file:../shared/packages/sync", @@ -4145,6 +4146,28 @@ "resolved": "../shared/packages/models", "link": true }, + "node_modules/@offgrid/operator-ui": { + "name": "@offgrid/ui", + "version": "0.2.4", + "resolved": "git+ssh://git@github.com/wednesday-solutions/component-library-animations.git#ca35d2f9004b212ea034b73109fcd2819a08816b", + "integrity": "sha512-u/CZp7RhApUuvweQVGj+JKkz7MOE5AnJw8SBbzhqVbyoFvCcNWJxpE1h+cpebIA6u9kP3Odv/XewfG9eOY/UtQ==", + "license": "UNLICENSED", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "class-variance-authority": "^0.7.1" + }, + "engines": { + "node": ">=20.9" + }, + "peerDependencies": { + "@offgrid/design": ">=0.0.1 <1", + "react": "^18.3.0 || ^19.0.0", + "react-dom": "^18.3.0 || ^19.0.0" + } + }, "node_modules/@offgrid/rag": { "resolved": "packages/rag", "link": true diff --git a/package.json b/package.json index 0d00c5622..2320cb38f 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@dnd-kit/sortable": "^10.0.0", "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", + "@huggingface/transformers": "4.3.0", "@lancedb/lancedb": "^0.30.0", "@langchain/core": "^1.2.9", "@langchain/langgraph": "^1.4.12", @@ -76,6 +77,7 @@ "@offgrid/clipboard": "file:./packages/clipboard", "@offgrid/design": "file:./packages/design", "@offgrid/models": "file:../shared/packages/models", + "@offgrid/operator-ui": "github:wednesday-solutions/component-library-animations#ca35d2f9004b212ea034b73109fcd2819a08816b", "@offgrid/rag": "file:./packages/rag", "@offgrid/speech": "file:../shared/packages/speech", "@offgrid/sync": "file:../shared/packages/sync", @@ -87,7 +89,6 @@ "@tabler/icons-react": "^3.36.1", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-virtual": "3.14.13", - "@huggingface/transformers": "4.3.0", "apache-arrow": "^18.1.0", "async-mutex": "^0.5.0", "better-sqlite3": "^12.6.2", diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index dc95b411c..6988d0377 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,3 +1,4 @@ +import { appLocationPath, replaceAppLocation } from './lib/app-location' import { ChatList } from './components/ChatList' import { ChatDetail } from './components/ChatDetail' import { CommandPalette } from './components/CommandPalette' @@ -481,7 +482,7 @@ function AppContent() { // Handle browser URL changes useEffect(() => { - const path = window.location.pathname + const path = appLocationPath() const viewMap: Record = { '/': 'day', '/explore': 'explore', @@ -628,8 +629,8 @@ function AppContent() { } else if (isInternalTabView(viewMode)) { newPath = internalTabPath(viewMode, navigationSubroute) } - if (window.location.pathname !== newPath) { - window.history.replaceState(null, '', newPath) + if (appLocationPath() !== newPath) { + replaceAppLocation(newPath) } // Publish the view for anything that needs to reason about the current screen. replaceState // fires no event, so the URL alone is not observable. diff --git a/src/renderer/src/assets/logos/README.md b/src/renderer/src/assets/logos/README.md new file mode 100644 index 000000000..acb1a824a --- /dev/null +++ b/src/renderer/src/assets/logos/README.md @@ -0,0 +1,10 @@ +# Connection logos + +Bundled from the Simple Icons source repository. Google, Notion, Obsidian, +Linear, Jira, and Confluence use tag 16.0.0; Microsoft uses tag 11.0.0 (the +mark was removed from newer catalogues). The screen renders these marks in +monochrome using the current theme. No runtime logo request is made. + +Source: https://github.com/simple-icons/simple-icons +License: CC0-1.0. Brand marks remain the property of their respective owners. +The pre-existing Slack asset is unchanged. diff --git a/src/renderer/src/assets/logos/confluence.svg b/src/renderer/src/assets/logos/confluence.svg new file mode 100644 index 000000000..2ac98a7df --- /dev/null +++ b/src/renderer/src/assets/logos/confluence.svg @@ -0,0 +1 @@ +Confluence \ No newline at end of file diff --git a/src/renderer/src/assets/logos/google.svg b/src/renderer/src/assets/logos/google.svg new file mode 100644 index 000000000..2eaf91554 --- /dev/null +++ b/src/renderer/src/assets/logos/google.svg @@ -0,0 +1 @@ +Google \ No newline at end of file diff --git a/src/renderer/src/assets/logos/jira.svg b/src/renderer/src/assets/logos/jira.svg new file mode 100644 index 000000000..417c09c56 --- /dev/null +++ b/src/renderer/src/assets/logos/jira.svg @@ -0,0 +1 @@ +Jira \ No newline at end of file diff --git a/src/renderer/src/assets/logos/linear.svg b/src/renderer/src/assets/logos/linear.svg new file mode 100644 index 000000000..f3770d654 --- /dev/null +++ b/src/renderer/src/assets/logos/linear.svg @@ -0,0 +1 @@ +Linear \ No newline at end of file diff --git a/src/renderer/src/assets/logos/microsoft.svg b/src/renderer/src/assets/logos/microsoft.svg new file mode 100644 index 000000000..eeacf2520 --- /dev/null +++ b/src/renderer/src/assets/logos/microsoft.svg @@ -0,0 +1 @@ +Microsoft \ No newline at end of file diff --git a/src/renderer/src/assets/logos/notion.svg b/src/renderer/src/assets/logos/notion.svg new file mode 100644 index 000000000..2917f42ee --- /dev/null +++ b/src/renderer/src/assets/logos/notion.svg @@ -0,0 +1 @@ +Notion \ No newline at end of file diff --git a/src/renderer/src/assets/logos/obsidian.svg b/src/renderer/src/assets/logos/obsidian.svg new file mode 100644 index 000000000..fc35287f3 --- /dev/null +++ b/src/renderer/src/assets/logos/obsidian.svg @@ -0,0 +1 @@ +Obsidian \ No newline at end of file diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index c89e40f9f..2a94f29be 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -1,4 +1,8 @@ +/* Shared controls must follow the reset in the CSS cascade. */ +@layer theme, base, components, utilities; + @import './base.css'; +@import '@offgrid/operator-ui/styles.css'; @import 'tailwindcss'; /* Tailwind v4 auto-detects content but SKIPS .gitignored paths — and the pro/ @@ -582,3 +586,40 @@ code { radial-gradient(1px 1px at 150px 192px, rgba(255, 255, 255, 0.6), transparent), radial-gradient(1px 1px at 205px 120px, rgba(255, 255, 255, 0.78), transparent); } + +/* Connection composition reuses the existing Item and SidePanel owners. */ +.connection-group-label { + margin-bottom: var(--og-space-md); + color: var(--og-text-secondary); + font-size: var(--og-type-label-size); + font-weight: var(--og-type-label-weight); + text-transform: uppercase; + letter-spacing: 0.1em; +} +.connection-app-logo { + width: var(--og-space-xl); + height: var(--og-space-xl); + flex-shrink: 0; + object-fit: contain; + filter: invert(1); +} +[data-theme='light'] .connection-app-logo { + filter: none; +} +.connection-status { + display: inline-flex; + align-items: center; + gap: var(--og-space-xs); + color: var(--og-primary); + font-size: var(--og-type-meta-size); +} +.connection-error { + color: var(--og-error); + font-size: var(--og-type-h3-size); + overflow-wrap: anywhere; +} +@media (max-width: 640px) { + .connection-item { + flex-wrap: wrap; + } +} diff --git a/src/renderer/src/bootstrap/slotRegistry.ts b/src/renderer/src/bootstrap/slotRegistry.ts index 460bd17f1..fd24979e6 100644 --- a/src/renderer/src/bootstrap/slotRegistry.ts +++ b/src/renderer/src/bootstrap/slotRegistry.ts @@ -31,6 +31,8 @@ export const SLOTS = { /** Per-connector credential setup UI for `oauthClient: 'byo'` entries (e.g. the * Google client_id/secret form). Receives the catalog entry as a prop. */ connectorSetup: 'connectors.setup', + /** Optional provider-owned quick setup, used in onboarding and Integrations. */ + quickConnectionProviders: 'connectors.quickProviders', /** Rows appended after the message list of the open conversation (e.g. a reply * streaming live on another device). Receives `{ conversationId }`. */ chatMessagesFooter: 'chat.messagesFooter', diff --git a/src/renderer/src/components/ConnectorsScreen.tsx b/src/renderer/src/components/ConnectorsScreen.tsx index 351e3c87e..cc981b91b 100644 --- a/src/renderer/src/components/ConnectorsScreen.tsx +++ b/src/renderer/src/components/ConnectorsScreen.tsx @@ -20,6 +20,9 @@ import { } from './connectorCatalog' import slackLogo from '@/assets/logos/slack.svg' import { getSlot, SLOTS } from '@/bootstrap/slotRegistry' +import { QuickConnections } from './QuickConnections' +import { QUICK_CONNECTION_IDS } from './quickConnectionCatalog' +import { CONNECTIONS_CHANGED_EVENT } from './useQuickConnection' // Brands Simple Icons dropped (trademark) → bundled local logos, keyed by catalog id. const LOGO_OVERRIDE: Record = { slack: slackLogo } @@ -280,10 +283,21 @@ export function ConnectorsScreen(): ReactElement { }, []) useEffect(() => { void load() + const changed = (): void => { + void load() + } + window.addEventListener(CONNECTIONS_CHANGED_EVENT, changed) + return () => window.removeEventListener(CONNECTIONS_CHANGED_EVENT, changed) }, [load]) const installed = new Set(items.map((i) => i.name.toLowerCase())) - const gallery = CONNECTOR_CATALOG.filter((e) => e.ready && !installed.has(e.name.toLowerCase())) + const gallery = CONNECTOR_CATALOG.filter( + (e) => + e.ready && + !QUICK_CONNECTION_IDS.includes(e.id) && + !(getSlot(SLOTS.quickConnectionProviders) && ['gmail', 'google-calendar'].includes(e.id)) && + !installed.has(e.name.toLowerCase()) + ) const doConnect = async ( entry: CatalogEntry, @@ -458,11 +472,19 @@ export function ConnectorsScreen(): ReactElement {
+ {detailId == null && tab !== 'connected' && ( +
+ +
+ )} {(() => { const detail = detailId != null ? items.find((c) => c.id === detailId) : null if (detail) { const dcat = CONNECTOR_CATALOG.find((x) => x.name === detail.name) - const dNotReady = dcat != null && !dcat.ready // preview/unverified — don't expose Test/Sync + const dNotReady = + dcat != null && + !dcat.ready && + !(detail.tools && detail.tools !== '[]' && detail.url?.startsWith('offgrid-')) // preview/unverified — don't expose Test/Sync const dtools = detail.tools ? (JSON.parse(detail.tools) as { name: string; description?: string }[]) : [] @@ -826,10 +848,12 @@ export function ConnectorsScreen(): ReactElement {
{items.map((c) => { const cat = CONNECTOR_CATALOG.find((x) => x.name === c.name) - // A connector whose catalog entry is not `ready` is a preview/unverified - // integration (e.g. Gmail, Google Calendar) — never present it as working - // "connected", even if a stale row exists. Show it as disabled. - const notReady = cat != null && !cat.ready + // Preview catalog entries stay disabled. A verified internal local provider + // can supply working tools without that catalog's third-party server. + const notReady = + cat != null && + !cat.ready && + !(c.tools && c.tools !== '[]' && c.url?.startsWith('offgrid-')) return ( + )} + +
+
) diff --git a/src/renderer/src/components/PermissionGate.tsx b/src/renderer/src/components/PermissionGate.tsx index e4a023e28..6238ef9e9 100644 --- a/src/renderer/src/components/PermissionGate.tsx +++ b/src/renderer/src/components/PermissionGate.tsx @@ -1,3 +1,4 @@ +import { replaceAppLocation } from '@renderer/lib/app-location' import { useState, useEffect, useCallback } from 'react' import { motion } from 'motion/react' import { GridBackdrop } from './ui/grid-backdrop' @@ -147,7 +148,7 @@ export function PermissionGate({ children }: PermissionGateProps) { const openModels = (): void => { window.dispatchEvent(new CustomEvent('og:navigate', { detail: 'models' })) - window.history.replaceState(null, '', '/models') + replaceAppLocation('/models') } const handleVisionAction = (): void => { diff --git a/src/renderer/src/components/QuickConnections.tsx b/src/renderer/src/components/QuickConnections.tsx new file mode 100644 index 000000000..d84e874af --- /dev/null +++ b/src/renderer/src/components/QuickConnections.tsx @@ -0,0 +1,244 @@ +import { SidePanel } from './SidePanel' +import { AnimatePresence } from 'motion/react' +import { CONNECTIONS_CHANGED_EVENT } from './useQuickConnection' +import { getRendererIsPro } from '@renderer/bootstrap/entitlementRegistry' +import { useEffect, useState, type JSX } from 'react' +import { CheckCircle, SlidersHorizontal, X } from '@phosphor-icons/react' +import { Button } from './ui/button' +import { Item, ItemGroup, ItemContent, ItemTitle, ItemDescription, ItemActions } from './ui/item' +import notionLogo from '@/assets/logos/notion.svg' +import jiraLogo from '@/assets/logos/jira.svg' +import confluenceLogo from '@/assets/logos/confluence.svg' +import linearLogo from '@/assets/logos/linear.svg' +import { getSlot, registerSlot, SLOTS } from '@renderer/bootstrap/slotRegistry' +import { CONNECTOR_CATALOG } from './connectorCatalog' +import { useQuickConnection, type QuickConnectionEntry } from './useQuickConnection' + +import { QUICK_CONNECTION_IDS } from './quickConnectionCatalog' + +export function QuickConnections({ + onBusyChange +}: { + onBusyChange?: (busy: boolean) => void +}): JSX.Element { + const [, refreshProviders] = useState(0) + useEffect(() => { + let active = true + if (!getSlot(SLOTS.quickConnectionProviders) && getRendererIsPro()) { + void import('@offgrid/pro/renderer') + .then((module) => { + if (!active || !getRendererIsPro()) return + const activate = ( + module as { + activateQuickConnectionRenderer?: (api: { registerSlot: typeof registerSlot }) => void + } + ).activateQuickConnectionRenderer + activate?.({ registerSlot }) + refreshProviders((value) => value + 1) + }) + .catch(() => { + /* Core builds have no provider UI. */ + }) + } + return () => { + active = false + } + }, []) + const [manageUrl, setManageUrl] = useState(null) + const [removing, setRemoving] = useState(null) + const [removeError, setRemoveError] = useState('') + const [providerBusy, setProviderBusy] = useState(false) + const connection = useQuickConnection() + const Providers = getRendererIsPro() ? getSlot(SLOTS.quickConnectionProviders) : undefined + const pending = !!connection.busy || providerBusy + useEffect(() => { + onBusyChange?.(pending) + }, [pending, onBusyChange]) + + return ( +
+
+

Connect your work

+

+ Bring your accounts and tools into your private workspace. +

+
+ {connection.errors.load && ( +
+

{connection.errors.load}

+ +
+ )} + + {manageUrl && ( + setManageUrl(null)}> +
+

Connected accounts

+ +
+
+ {connection.items + .filter((account) => account.url === manageUrl) + .map((account) => ( + + + {account.name} + Connection {account.id} + + + + + + ))} + {removeError && ( +

+ {removeError} +

+ )} +

+ Removing a connection clears its saved access on this device. You can connect it + again. +

+
+
+ )} +
+
+ {Providers && ( +
+

Accounts & notes

+ + + +
+ )} +
+

Work tools

+ + {QUICK_CONNECTION_IDS.map((id) => { + const catalog = CONNECTOR_CATALOG.find((item) => item.id === id) + if (!catalog?.ready || !catalog.url || catalog.auth !== 'oauth') return null + const entry: QuickConnectionEntry = { + id, + name: id === 'jira' ? 'Jira + Confluence' : catalog.name, + url: catalog.url + } + const record = connection.recordFor(entry) + const accounts = connection.items.filter( + (item) => item.url === entry.url && item.status === 'ok' && !!item.enabled + ) + const connected = accounts.length > 0 + const authorizing = connection.busy === id + const logo = id === 'notion' ? notionLogo : id === 'jira' ? jiraLogo : linearLogo + return ( +
+ +
+ + {id === 'jira' && ( + + )} +
+ + {entry.name} + + {id === 'jira' ? 'Issues and shared pages' : catalog.blurb} + + + + {connected ? ( + + + ) : null} + + {connected && ( + + )} + {authorizing && ( + + )} + +
+ {connection.errors[id] && ( +

+ {connection.errors[id]} +

+ )} +
+ ) + })} +
+
+
+
+ ) +} diff --git a/src/renderer/src/components/__tests__/Onboarding.model-control.integration.test.tsx b/src/renderer/src/components/__tests__/Onboarding.model-control.integration.test.tsx index 7f3e04f7f..75d58ccc1 100644 --- a/src/renderer/src/components/__tests__/Onboarding.model-control.integration.test.tsx +++ b/src/renderer/src/components/__tests__/Onboarding.model-control.integration.test.tsx @@ -68,6 +68,8 @@ describe('rendered multimodal onboarding journey', () => { 'See and switch the active Chat, Image, Transcription, Voice, and Computer Use model' ) + await user.click(screen.getByRole('button', { name: 'Continue' })) + await waitFor(() => expect(renderedText()).toContain('Connect your work')) await user.click(screen.getByRole('button', { name: 'Continue' })) await waitFor(() => expect(renderedText()).toContain('You choose where each model runs')) expect(renderedText()).toContain( diff --git a/src/renderer/src/components/__tests__/QuickConnections.test.tsx b/src/renderer/src/components/__tests__/QuickConnections.test.tsx new file mode 100644 index 000000000..5d0d28a2c --- /dev/null +++ b/src/renderer/src/components/__tests__/QuickConnections.test.tsx @@ -0,0 +1,96 @@ +// @vitest-environment jsdom +import { cleanup, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { QuickConnections } from '../QuickConnections' + +let records: Array<{ id: number; name: string; url: string; status: string; enabled: number }> +let testResult: { ok: boolean; error?: string } +let resolveTest: ((value: { ok: boolean }) => void) | undefined +let waitForLogin: boolean +const api = { + mcpList: vi.fn(async () => [...records]), + mcpAdd: vi.fn(async (entry) => { + records.push({ ...entry, id: 7, status: 'unknown', enabled: 0 }) + return 7 + }), + mcpTest: vi.fn(async () => { + const result = waitForLogin + ? await new Promise<{ ok: boolean }>((resolve) => { + resolveTest = resolve + }) + : testResult + if (result.ok && records[0]) records[0].status = 'ok' + return result + }), + mcpSetEnabled: vi.fn(async (_id, enabled) => { + if (records[0]) records[0].enabled = Number(enabled) + }), + mcpRemove: vi.fn(async () => { + records = [] + }), + mcpCancel: vi.fn(async () => { + resolveTest?.({ ok: false }) + }) +} +beforeEach(() => { + vi.clearAllMocks() + records = [] + testResult = { ok: true } + waitForLogin = false + resolveTest = undefined + Object.defineProperty(window, 'api', { configurable: true, value: api }) +}) +afterEach(cleanup) +describe('quick connections', () => { + it('connects directly once and shows verified success', async () => { + const user = userEvent.setup() + render() + await waitFor(() => + expect(screen.getAllByRole('button', { name: 'Connect' })[0]?.hasAttribute('disabled')).toBe( + false + ) + ) + await user.click(screen.getAllByRole('button', { name: 'Connect' })[0]!) + await screen.findByText('Connected') + expect(api.mcpAdd).toHaveBeenCalledWith( + expect.objectContaining({ url: 'https://mcp.notion.com/mcp', liveOnly: true }) + ) + expect(api.mcpSetEnabled).toHaveBeenCalledWith(7, true) + expect(api.mcpRemove).not.toHaveBeenCalled() + }) + it('removes failed setup, shows the error, and permits retry', async () => { + testResult = { ok: false, error: 'Access was denied.' } + const user = userEvent.setup() + render() + await waitFor(() => + expect(screen.getAllByRole('button', { name: 'Connect' })[0]?.hasAttribute('disabled')).toBe( + false + ) + ) + await user.click(screen.getAllByRole('button', { name: 'Connect' })[0]!) + await screen.findByText('Access was denied.') + expect(api.mcpRemove).toHaveBeenCalledWith(7) + expect(api.mcpSetEnabled).not.toHaveBeenCalled() + testResult = { ok: true } + await user.click(screen.getAllByRole('button', { name: 'Connect' })[0]!) + await screen.findByText('Connected') + }) + it('cancels pending sign-in without marking the connection ready', async () => { + waitForLogin = true + const user = userEvent.setup() + const onBusyChange = vi.fn() + render() + await waitFor(() => + expect(screen.getAllByRole('button', { name: 'Connect' })[0]?.hasAttribute('disabled')).toBe( + false + ) + ) + await user.click(screen.getAllByRole('button', { name: 'Connect' })[0]!) + await user.click(await screen.findByRole('button', { name: 'Cancel Notion sign-in' })) + await waitFor(() => expect(onBusyChange).toHaveBeenLastCalledWith(false)) + expect(api.mcpCancel).toHaveBeenCalledWith(7) + expect(api.mcpSetEnabled).not.toHaveBeenCalled() + expect(screen.queryByText('Connected')).toBeNull() + }) +}) diff --git a/src/renderer/src/components/connectorCatalog.ts b/src/renderer/src/components/connectorCatalog.ts index e5bd0ef8c..0a4b0047c 100644 --- a/src/renderer/src/components/connectorCatalog.ts +++ b/src/renderer/src/components/connectorCatalog.ts @@ -186,7 +186,7 @@ export const CONNECTOR_CATALOG: CatalogEntry[] = [ color: '#172B4D', letter: 'C', transport: 'http', - url: 'https://mcp.atlassian.com/v1/mcp', + url: 'https://mcp.atlassian.com/v2/mcp', auth: 'oauth', docsUrl: 'https://www.atlassian.com/blog/announcements/remote-mcp-server', ready: false @@ -244,7 +244,7 @@ export const CONNECTOR_CATALOG: CatalogEntry[] = [ color: '#0052CC', letter: 'J', transport: 'http', - url: 'https://mcp.atlassian.com/v1/mcp', + url: 'https://mcp.atlassian.com/v2/mcp', auth: 'oauth', docsUrl: 'https://www.atlassian.com/blog/announcements/remote-mcp-server', ready: true diff --git a/src/renderer/src/components/quickConnectionCatalog.ts b/src/renderer/src/components/quickConnectionCatalog.ts new file mode 100644 index 000000000..1c6667916 --- /dev/null +++ b/src/renderer/src/components/quickConnectionCatalog.ts @@ -0,0 +1 @@ +export const QUICK_CONNECTION_IDS = ['notion', 'jira', 'linear'] diff --git a/src/renderer/src/components/useQuickConnection.ts b/src/renderer/src/components/useQuickConnection.ts new file mode 100644 index 000000000..57ed310b2 --- /dev/null +++ b/src/renderer/src/components/useQuickConnection.ts @@ -0,0 +1,153 @@ +import { useCallback, useEffect, useRef, useState } from 'react' + +export const CONNECTIONS_CHANGED_EVENT = 'offgrid:connections-changed' + +export interface QuickConnectionEntry { + id: string + name: string + url: string + /** Create a separate account instead of reusing a provider connection. */ + newAccount?: boolean +} + +export interface QuickConnectionRecord { + id: number + name: string + url: string | null + status: string + enabled: number +} + +interface Attempt { + cancelled: boolean + id?: number + created: boolean +} + +/** Owns user-initiated sign-in and cleans up incomplete connections. */ +interface QuickConnectionState { + items: QuickConnectionRecord[] + loading: boolean + busy: string | null + errors: Record + connect: ( + entry: QuickConnectionEntry, + prepare?: (id: number, created: boolean) => Promise, + finish?: (id: number) => Promise + ) => Promise + cancel: () => Promise + reload: () => Promise + recordFor: (entry: QuickConnectionEntry) => QuickConnectionRecord | undefined +} +const wasCancelled = (attempt: Attempt): boolean => attempt.cancelled + +export function useQuickConnection(onBusyChange?: (busy: boolean) => void): QuickConnectionState { + const [items, setItems] = useState([]) + const [loading, setLoading] = useState(true) + const [busy, setBusy] = useState(null) + const [errors, setErrors] = useState>({}) + const active = useRef(null) + const mounted = useRef(false) + const onBusy = useRef(onBusyChange) + onBusy.current = onBusyChange + + const reload = useCallback(async () => { + try { + const rows = (await window.api.mcpList()) as QuickConnectionRecord[] + if (mounted.current) { + setItems(rows) + setErrors((previous) => ({ ...previous, load: '' })) + } + } catch { + if (mounted.current) + setErrors((previous) => ({ ...previous, load: 'Could not load connections. Try again.' })) + } finally { + if (mounted.current) setLoading(false) + } + }, []) + + const cancel = useCallback(async () => { + const attempt = active.current + if (!attempt) return + attempt.cancelled = true + if (attempt.id != null) { + await window.api.mcpCancel(attempt.id) + if (attempt.created) await window.api.mcpRemove(attempt.id) + } + }, []) + + useEffect(() => { + mounted.current = true + void reload() + const changed = (): void => { + void reload() + } + window.addEventListener(CONNECTIONS_CHANGED_EVENT, changed) + return () => { + mounted.current = false + window.removeEventListener(CONNECTIONS_CHANGED_EVENT, changed) + void cancel().catch(() => {}) + onBusy.current?.(false) + } + }, [reload, cancel]) + + const connect = async ( + entry: QuickConnectionEntry, + prepare?: (id: number, created: boolean) => Promise, + finish?: (id: number) => Promise + ): Promise => { + if (active.current) return + const attempt: Attempt = { cancelled: false, created: false } + active.current = attempt + setBusy(entry.id) + onBusy.current?.(true) + setErrors((previous) => ({ ...previous, [entry.id]: '' })) + try { + // Read current state rather than a stale rendered list before creating a record. + const current = (await window.api.mcpList()) as QuickConnectionRecord[] + const existing = entry.newAccount ? undefined : current.find((item) => item.url === entry.url) + if (wasCancelled(attempt)) return + attempt.id = + existing?.id ?? + (await window.api.mcpAdd({ + name: entry.name, + transport: 'http', + url: entry.url, + liveOnly: true + })) + attempt.created = !existing + if (!Number.isInteger(attempt.id) || !attempt.id || attempt.id < 1) + throw new Error('Could not create the connection. Try again.') + if (wasCancelled(attempt)) return + await prepare?.(attempt.id!, attempt.created) + if (wasCancelled(attempt)) return + const result = (await window.api.mcpTest(attempt.id!)) as { ok: boolean; error?: string } + if (wasCancelled(attempt)) return + if (!result.ok) throw new Error(result.error || 'Could not connect. Try again.') + if (finish) { + attempt.id = await finish(attempt.id!) + attempt.created = false + } + await window.api.mcpSetEnabled(attempt.id!, true) + attempt.created = false + } catch (error) { + if (!attempt.cancelled && mounted.current) { + setErrors((previous) => ({ + ...previous, + [entry.id]: error instanceof Error ? error.message : 'Could not connect. Try again.' + })) + } + } finally { + if (attempt.created && attempt.id != null) + await window.api.mcpRemove(attempt.id).catch(() => {}) + active.current = null + if (mounted.current) setBusy(null) + onBusy.current?.(false) + window.dispatchEvent(new Event(CONNECTIONS_CHANGED_EVENT)) + } + } + + const recordFor = (entry: QuickConnectionEntry): QuickConnectionRecord | undefined => + items.find((item) => item.url === entry.url) + return { items, loading, busy, errors, connect, cancel, reload, recordFor } +} diff --git a/src/renderer/src/lib/app-location.ts b/src/renderer/src/lib/app-location.ts new file mode 100644 index 000000000..7491a42d9 --- /dev/null +++ b/src/renderer/src/lib/app-location.ts @@ -0,0 +1,10 @@ +/** Keep the renderer HTML path intact so packaged app reloads work. */ +export function appLocationPath(): string { + return window.location.protocol === 'file:' + ? window.location.hash.slice(1) || '/' + : window.location.pathname +} + +export function replaceAppLocation(path: string): void { + window.history.replaceState(null, '', window.location.protocol === 'file:' ? `#${path}` : path) +} diff --git a/src/renderer/src/theme.ts b/src/renderer/src/theme.ts index caf054c90..ab78f5de8 100644 --- a/src/renderer/src/theme.ts +++ b/src/renderer/src/theme.ts @@ -14,7 +14,7 @@ function systemPrefersDark(): boolean { export function getThemeMode(): ThemeMode { const v = localStorage.getItem(KEY) as ThemeMode | null - return v === 'light' || v === 'dark' || v === 'system' ? v : 'system' + return v === 'light' || v === 'dark' || v === 'system' ? v : 'dark' } export function resolveTheme(mode: ThemeMode): 'light' | 'dark' { From 93ef76f4d25316f123d6883eb1315d6578979b20 Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Thu, 1 Oct 2026 12:47:50 +0530 Subject: [PATCH 3/4] docs: align brand guidance and record integration research --- docs/DESIGN.md | 2 +- docs/DESIGN_PHILOSOPHY.md | 214 ++++-- .../direct-communication-connections.md | 609 ++++++++++++++++++ 3 files changed, 769 insertions(+), 56 deletions(-) create mode 100644 docs/research/direct-communication-connections.md diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 8c885c0f9..ed6147ec5 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1,6 +1,6 @@ # Off Grid AI Desktop — Design -The desktop adaptation of the Off Grid AI design philosophy. The brand canon is the mobile docs (`../../mobile/docs/design/DESIGN_PHILOSOPHY_SYSTEM.md` + `VISUAL_HIERARCHY_STANDARD.md`); **this doc keeps the same soul and adapts it for a desktop app.** Where this conflicts with the mobile docs on _layout/interaction_, desktop wins; where it conflicts on _brand_ (font, color, voice), the brand wins. +Read [DESIGN_PHILOSOPHY.md](DESIGN_PHILOSOPHY.md) before any Desktop UI change. It is synchronized from the canonical sibling `brand/DESIGN_PHILOSOPHY.md`. Shared brand principles follow that source; this document supplies Desktop layout and interaction details. --- diff --git a/docs/DESIGN_PHILOSOPHY.md b/docs/DESIGN_PHILOSOPHY.md index bb22d7d5b..50d93551c 100644 --- a/docs/DESIGN_PHILOSOPHY.md +++ b/docs/DESIGN_PHILOSOPHY.md @@ -1,71 +1,175 @@ -# Off Grid AI Desktop — Design Philosophy (whole app) +# Off Grid — Design Philosophy (shared source of truth) -The binding design philosophy for **every screen** of Off Grid AI Desktop — chat, Day, -Replay, Reflect, Actions, Connectors, Meetings, Models, Entities, Settings, Onboarding, -and everything built hereafter. Decided 2026-06-23. Pairs with `docs/DESIGN.md` (brand -canon) and the memory `ui-component-standard`. +This is the **canonical, cross-platform design philosophy** for Off Grid. Every surface — Off +Grid Desktop, Off Grid Mobile, the Console, the website, and every landing page — inherits from +here. When a platform doc and this doc disagree on a shared principle, this doc wins; platform +docs own only the platform-specific mechanics (component libraries, token accessors, layout +density) and link back here. -> This is not a chat-only spec. It governs the entire application. New surfaces and -> refactors of existing ones must follow it. +Read this before designing or reviewing any Off Grid surface. It pairs with the copy guides in +this folder (`README.md`, `brand_tone_voice.md`) — the look and the words carry the same +disposition: give the user something clean and honest, show the mechanism, never decorate to +impress. -## 1. North star +--- -**Smooth. Easy. Inevitable.** Every surface should feel effortless and alive — the right -thing appears at the right moment, nothing feels like a tool you must operate. The product -should feel breathtaking through restraint and motion, not clutter. It is private, -on-device, and fast — the UI should feel that way too: calm, dense, immediate. +## The disposition -## 2. Components — reuse, never build (binding) +The interface is quiet on purpose. It is private, on-device, and fast — and it should feel that +way: calm, dense, immediate. Nothing shouts. The product earns trust by being clear and by +getting out of the way, not by ornament. Restraint and motion do the work that gradients and +color usually do elsewhere. Let the content — the memory, the AI response, the captured moment — +be the thing that shines. -- **Use ONLY the approved libraries.** No custom UI components, anywhere: - - **shadcn/ui** — foundation (buttons, inputs, dialogs, menus, tabs, tooltips, …) - - **Aceternity UI** — high-end effects - - **Magic UI** — text & button animations - - **Motion Primitives** — advanced transitions -- **Pull, don't write:** `npx shadcn add ` / `@aceternity/` / `@magicui/`. - Choose from the catalog index `component-library-animations/skills/component-library-index.md` - (the repo's component files are demo stubs — pull the real one from the registry). -- `components.json` is configured with the `@aceternity` + `@magicui` registries + shadcn. +**Silence, clarity, and function over form.** -## 3. Brand — Off Grid AI identity (binding) +--- -- **Typeface:** Menlo (monospace), everywhere. Terminal/brutalist. -- **Accent:** emerald — `#34D399` (dark) / `#059669` (light). The only accent. -- **Base:** black / `#0A0A0A` + white; neutral grays. Flat, sharp, dense. No gradients - beyond brand, no decorative tiles, no emojis in UI. -- **Theme-aware tokens:** shadcn semantic tokens (`--color-primary`, `--muted`, `--border`, - `--ring`, …) are mapped to the `--og-*` tokens in `src/renderer/src/assets/main.css` - `@theme`. Result: any approved-library component inherits the brand with **zero - per-component styling**, and flips light/dark automatically. Always rely on this mapping - rather than hardcoding colors. +## Shared principles (binding on every platform) -## 4. Motion (binding) +### 1. Brutalist / terminal aesthetic +Minimal, functional, terminal-inspired. Flat, sharp, dense. Every element earns its place; +remove before adding. No decorative tiles, no 3D effects, no emojis in the UI, no gradients +beyond the brand. -- Animate **only** `transform` and `opacity`. Never layout-thrashing properties. -- Timings: micro 100–150ms · hover/spring 200–300ms · reveal 300–500ms. -- Always honor `prefers-reduced-motion`. -- Motion clarifies state and guides attention — it never decorates. +### 2. Typeface: Menlo, monospace, everywhere +One font family across the whole product. Weights stay light (roughly 200–400). Hierarchy comes +from size, weight, and spacing — never from mixing fonts. -## 5. Interaction principles (app-wide) +### 3. Accent: emerald, and only emerald +- Dark mode: `#34D399` +- Light mode: `#059669` -- **No needless modes/toggles.** Infer intent; expose advanced controls only when wanted, - tucked away. -- **Stream everything** that takes time; never a frozen "thinking" wall — use tasteful - in-progress states (skeletons, shimmer, progress). -- **Sensible defaults.** Nothing should require configuration before it works. -- **Desktop-first density.** Multi-column, side panels, hover affordances, dense lists — - never mobile-first. +The single accent color. Used sparingly — active states, focus, the one important action on a +screen. Everything else is a monochrome hierarchy. Do not introduce a second accent; do not +color-code information (use position, size, and weight instead). Semantic colors (error/red) +exist only for their exact purpose. -## 6. Code standard (standards-kit, binding) +### 4. Base: black / white + neutral grays +Dark mode background is pure-ish black (`#0A0A0A`); light mode is clean white (`#FFFFFF`). +Build depth with a small tiered surface system (background → surface → nested/input), not with +shadow. High contrast in both themes. -- Cyclomatic complexity **< 8**. -- **PascalCase** for UI/Types, **camelCase** for logic. -- Strict import ordering; no `console.log`, magic numbers, or unused imports. -- Accessibility: `aria-label` on icon buttons, `alt` on images, 4.5:1 contrast. +### 5. Tokens, not magic numbers +All color, spacing, and typography come from **`@offgrid/design`** (and each platform's token +layer that maps to it). No hardcoded hex, no magic numbers in component styles. Components +inherit the brand and flip light/dark automatically through the token mapping — never hardcode a +color that a token already expresses. -## 7. Applications of this philosophy +### 6. Visual hierarchy through size and weight, not color +Primary → secondary → tertiary is expressed by typographic scale and spacing. Labels are quiet +uppercase "whispers" in muted tone; metadata is small and muted. Give content breathing room at +the top level and tighten within components. Respect information density where it serves scanning. -- **Chat / conversational surface:** `docs/CHAT_UX_SPEC.md` — one conversation where the - model generates images, artifacts, and clarifying questions inline at the right point. -- **Every other screen:** the **UI standardization audit** in `ROADMAP_DESKTOP.md` tracks - bringing the whole app to this philosophy, screen by screen. +### 7. Motion clarifies, never decorates +- Animate **only** `transform` and `opacity`. Never layout-thrashing properties. +- Timings: micro 100–150ms · hover/spring 200–300ms · reveal 300–500ms. +- Always honor `prefers-reduced-motion`. +- Stream anything that takes time; never a frozen wall — use skeletons/shimmer/progress. +- Functional animation only. If it doesn't clarify state or guide attention, cut it. + +### 8. Sensible defaults, no needless modes +Nothing should require configuration before it works. Infer intent; tuck advanced controls away +until wanted. Accessible by default: `aria-label` on icon buttons, `alt` on images, ≥4.5:1 +contrast, touch targets ≥44px. + +### 9. Use the space of the host surface +Do not make one platform imitate another. Desktop uses its wide canvas for responsive grids, +master-detail views, tables, and side panels. Mobile uses a focused vertical flow and progressive +disclosure. In both cases, avoid purposeless empty space and horizontal page overflow. + +### Anti-patterns (avoid everywhere) +Colorful gradients or heavy shadows · multiple accent colors · rounded pill shapes (use minimal +~8px radius) · decorative animation · mixed font families · 3D effects · color-coded +information · purposeless empty space · cluttered layouts. + +--- + +## Desktop profile + +Desktop is a wide, mouse-driven, keyboard-driven, multi-window surface. + +- Fill the canvas with responsive multi-column grids, dense tables, master-detail views, and side + panels. Do not center a phone-width column on a wide window. Keep controls near their content. +- Use tight 4/8/12px spacing inside dense groups and clearer separation between groups. Keep + headers, filters, tabs, and column labels visible while their body scrolls. +- Treat hover and keyboard input as first-class states. Reveal secondary actions on hover and + provide shortcuts for repeated navigation and submission. +- Use the shared `SidePanel` for settings, editors, previews, and multi-step details. It must close + by Escape, outside click, and its close control. Reserve a centered dialog for a short blocking + confirmation. +- Desktop typography roles are: title 18px; body and subtitle 14px; description 12px; metadata and + labels 10–11px. Labels are uppercase, muted, and tracked. +- In Tailwind, use `font-mono`, `rounded-md`, neutral surface and border tokens, and the emerald + token for active, focus, and primary-action states. Use `@phosphor-icons/react` for icons. +- Interactive controls transition in 100–150ms and use a small pressed scale. Panels and dialogs + use opacity plus scale or slide over 120–200ms. Keep exit animation and reduced-motion behavior. +- Radix dialogs are centered with the CSS `translate` property. Their keyframes may animate only + `transform: scale()` and opacity; adding a transform translation causes the dialog to jump. + +## Mobile profile + +Mobile is a touch-driven, narrow surface. Use a clear vertical flow, compact groups, and progressive +disclosure. Touch targets are at least 44px. + +### Mobile tokens + +- Access colors and shadows through `useTheme()` and styles through `useThemedStyles()`. +- Use `TYPOGRAPHY`, `SPACING`, and `FONTS` from `src/constants`; never hardcode their values in a + component. +- The spacing scale is 4, 8, 12, 16, 24, and 32px (`xs` through `xxl`). Standard screen padding is + 24px, card padding is 16px, and normal control or list spacing is 8–12px. +- Surface tokens are `background`, `surface`, and `surfaceLight`. Text tokens are `text`, + `textSecondary`, and `textMuted`. Use `borderFocus` for focus and `error` only for errors. + +### Mobile typography hierarchy + +| Role | Token | Size | Weight | Use | +| --- | --- | ---: | ---: | --- | +| Display | `TYPOGRAPHY.display` | 22px | 200 | Large numeric values | +| Title | `TYPOGRAPHY.h2` | 16px | 400 | One screen title | +| Body | `TYPOGRAPHY.body` | 14px | 400 | Messages, inputs, buttons, and primary content | +| Subtitle | `TYPOGRAPHY.h3` | 13px | 400 | Sections, cards, and modal titles | +| Description | `TYPOGRAPHY.bodySmall` | 13px | 400 | Help and explanatory text | +| Metadata | `TYPOGRAPHY.meta` / `label` | 9–10px | 300–400 | Timestamps, metrics, and uppercase labels | + +`h1` is reserved for rare hero text and is not a normal screen title. Hierarchy comes from size, +weight, and tone. Metadata must remain quiet. + +### Mobile components + +- Top-level tabs use the shared `ScreenHeader` with 12px horizontal and 8px vertical padding. + Pushed screens use 16px horizontal and 12px vertical padding, a back control, a title, and an + optional trailing action. Headers use the surface token, a subtle divider, and a stable stacking + level. Onboarding, lock, and model-download screens may use full-screen layouts. +- Buttons and inputs use an 8px radius, token spacing, and clear focus, pressed, disabled, loading, + and error states. Cards use `surface`, a subtle border, and at most a small theme shadow. +- Use `react-native-vector-icons`; Feather is the default. Do not use emojis as interface icons. +- Empty states contain one direct message and, when useful, one clear action. + +## Component ownership + +Use `@offgrid/design` for tokens and `wednesday-solutions/component-library-animations` for reusable +visual and interaction primitives. Search the existing product components before adding anything. +Extend a shared primitive when the concept is the same; do not fork a local visual copy. + +Console, website, and landing pages inherit the shared principles directly. + +--- + +**Remember:** the UI should feel like the product — private, quiet, fast. Let the AI's +capabilities and the user's content speak, not the interface. + +## Desktop source record + +Synchronized on 30 September 2026 from the sibling `brand/DESIGN_PHILOSOPHY.md`, +commit `15e6b2f` (7 September 2026). The previous Desktop guide last changed at +`332e48397` (27 August 2026). Use this guide before each Desktop UI change. +Shared principles follow the brand source; Desktop implementation details also +follow `docs/DESIGN.md`. + +Desktop uses `@offgrid/design` tokens and the shared operator primitives from +`wednesday-solutions/component-library-animations`. Consume the package export; +do not install separate registry copies or use catalogue demos as controls. +Keep Tailwind layers in `theme, base, components, utilities` order so resets cannot +override the shared controls. diff --git a/docs/research/direct-communication-connections.md b/docs/research/direct-communication-connections.md new file mode 100644 index 000000000..dace31696 --- /dev/null +++ b/docs/research/direct-communication-connections.md @@ -0,0 +1,609 @@ +# Direct communication connections for Off Grid AI Desktop + +Research date: 30 September 2026. This report is for internal product and engineering use. + +## RICE priority for consumer setup + +These are planning assumptions for 100 knowledge workers over one quarter, not measured product usage. RICE = reach × impact × confidence ÷ effort. Reach is users per quarter; impact is 0.5–3; confidence is a fraction; effort is engineer-weeks for an initial direct reader, excluding provider approval time and full synchronization. Replace these values with customer data before making delivery commitments. + +| Order | Connection | Reach | Impact | Confidence | Effort | RICE | Customer setup | +| --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | Local files / Obsidian | 65 | 2 | 90% | 0.5 | 234 | Select a vault folder; no plugin or login required | +| 2 | Notion | 60 | 2 | 85% | 0.5 | 204 | Connect to the official MCP and approve access | +| 3 | Google: Gmail, Calendar, Drive, Contacts | 80 | 3 | 75% | 1 | 180 | One registered application sign-in; own-client alternative | +| 4 | Jira / Confluence | 45 | 2 | 85% | 0.5 | 153 | One official MCP sign-in; tenant approval can apply | +| 5 | Linear | 25 | 1.5 | 90% | 0.5 | 67.5 | Official MCP sign-in through the existing connector path | +| 6 | Microsoft: Outlook, calendars, OneDrive | 60 | 3 | 75% | 3 | 45 | One public-client Graph sign-in; tenant consent can apply | +| 7 | Slack | 55 | 2 | 65% | 2 | 35.8 | Official MCP after application and workspace access requirements are met | +| 8 | Dropbox | 30 | 1.5 | 80% | 1.5 | 24 | Registered app plus native PKCE sign-in | +| 9 | Trello | 20 | 1 | 80% | 1 | 16 | Registered public API key plus user approval | +| 10 | Other email / calendars | 30 | 2 | 60% | 3 | 12 | IMAP / CalDAV; provider-specific account setup | + +The current implementation starts with the existing hosted MCP routes and Google API reader. The worktree now includes the local-vault reader described below. WhatsApp, WeChat, personal Teams history, and a complete social inbox have no verified universal consumer sign-in route under the direct-device rule. They must not appear as working one-click choices. + +## Onboarding implementation, 30 September 2026 + +Changes are isolated in the attached Desktop worktree and its private Pro checkout. An optional **Connect your work** step follows device setup. The same controls are available in Integrations. The generic core owns sign-in state, cancel, retry, and failed-record cleanup. Pro owns Google client settings, direct OAuth, and Gmail / Calendar / Drive / Contacts REST tools. The core-only build does not acquire a Google provider implementation. + +Google presents **Connect with Off Grid AI** and **Use your own client**. A grouped connection requests read scopes once and verifies all four APIs. Drive lists files with pagination and exports supported text documents, with a 2 MB read limit; binary files return metadata. Contacts retain pagination cursors. New quick connections stay disabled until verification succeeds and skip automatic memory imports. Existing connections retain their previous import policy. + +Shared UI selection: `wednesday-solutions/component-library-animations`, commit `ca35d2f9004b212ea034b73109fcd2819a08816b`, consumed through the `@offgrid/operator-ui` alias to avoid collision with the headless `@offgrid/ui` package. Production `Button`, `Card`, `Label`, and `NativeSelect` provide real props, keyboard support, loading state, and reduced motion. The main-branch previews and timer-based demo buttons were rejected. The existing own-client side panel remains the form owner. New controls use Off Grid design tokens and Phosphor icons. + +Credential check (30 September 2026): Google Cloud Console confirms the existing Off Grid AI registration is a public Desktop client. Its ID and enabled application credential match the user-authorized saved configuration. Both are configured in the ignored worktree `.env.local`, with `GOOGLE_OAUTH_CLIENT_TYPE=desktop`; the build includes them only in the main process. Release builds must receive the same Desktop configuration. Customers can use Connect with Off Grid AI without supplying developer credentials, or select their own client. OAuth uses the system browser, PKCE, a local callback, and protected local token storage. All service traffic goes directly to Google. The project is External and In production, but Google still requires verification for the requested scopes and shows a 100-user cap. The owner will complete verification. The configured build passed and the worktree app was restarted; real Google account sign-in remains to be reviewed. + +Validation covers rendered onboarding and provider choices, successful setup, failure cleanup, retry, cancellation, PKCE, partial grants, direct API execution, encrypted storage, and provider schema discovery. No end-to-end app run or screenshot was performed: repository instructions require an explicit user request. Both the Pro and core-only production builds passed. 63 product tests and 7 database integration tests passed. Changed implementation files passed lint with zero errors. Pro type checking passed. The full core type check reports an existing `ActiveChatStreamContract` / `ChatStreamPhase` mismatch for video phases in unchanged files. Worktree dependency copies required the shared sync package's own version of `@noble/hashes` and `xstate`; these fixes touched ignored dependencies only. + +## Current scope: knowledge-worker sources through MCP or APIs + +Direct APIs and MCP are both acceptable. Use one Off Grid connection interface across platforms, but do not require a local MCP server for a provider whose API is already simple to call. The device can call a source-operated MCP endpoint or a source API directly. Wrap either route in the same account, tool, action, and retention model. No third-party hosted connector is needed. + +The scope now covers email, calendar, messaging, task tools, notes, and files. Additional useful categories are meetings and transcripts, contacts, browser bookmarks and selected pages, developer work such as issues and pull requests, and CRM records. These are product categories, not commitments to implement every application. Prioritize the sources used by the selected customer group rather than treating all knowledge workers as one market. + +### Revised priority across the broader scope + +This order supersedes the communication-only rankings below. Effort is a relative judgment based on the inspected Desktop code and documented connection routes. Reach is expected coverage, not measured user demand. Complete sync, platform packaging, and permission work can raise the cost beyond an initial read tool. + +| Priority | Source | Direct route | Relative effort | Why it comes here | +| --- | --- | --- | --- | --- | +| 1 | Gmail and Google Calendar | Existing REST provider | Low for initial tools | Reuse existing code and cover communication plus scheduling | +| 2 | Local files and Obsidian | Selected folders and Markdown files; optional local MCP tools | Low for a Desktop reader | No provider sign-in; one route also covers local documents and downloaded files | +| 3 | Notion | Official source-operated MCP with OAuth and PKCE | Low to medium for live tools | Existing generic HTTP MCP system can supply notes and shared knowledge without a new full REST client | +| 4 | Microsoft mail and calendars | Graph and public-client OAuth | Medium | Major work suite through one account; evaluate OneDrive and SharePoint as later file capabilities | +| 5 | Jira and Confluence | Official Atlassian Rovo MCP | Low to medium for live tools | One source MCP covers issues and shared documentation; tenant approval may add setup steps | +| 6 | Google Drive, including Google document exports | Drive API | Medium | Reuses Google identity work; selected-file access and full-drive access have different scope and verification costs | +| 7 | Slack | Official source MCP | Medium overall | Important work context; publication, access, and retention remain release dependencies | +| 8 | Dropbox | Direct API with PKCE | Medium | Supported native sign-in; add after common file import and change tracking exist | +| 9 | Trello | Direct REST and delegated API-token authorization | Low to medium | Straightforward board/card model; useful but narrower than a work suite | +| 10 | Other email and calendar providers | Shared IMAP and CalDAV adapters | Medium | Broad coverage, with more credential and compatibility friction | +| Later | Meetings, contacts, CRM, browser sources, and developer work | Source-specific API, MCP, or deliberate local import | Not yet estimated | Select a role and test demand before another wide provider expansion | + +Obsidian stores its notes as Markdown files in a local vault folder. Start with user-selected read access; do not require an Obsidian plugin to read those files. A mobile sandbox can require a document picker or scoped file grant, so the same tool contract does not imply identical folder access on every device. A locally synced Dropbox or Drive folder is also useful, but it contains only the files present locally and does not prove complete cloud coverage. [Obsidian data storage](https://obsidian.md/help/data-storage) + +Notion publishes `https://mcp.notion.com/mcp` and documents custom clients using OAuth with PKCE and dynamic client registration. This is a strong early MCP target. Its legacy open-source local server is no longer actively maintained. Live tools are the first deliverable; background import still needs a separate review of coverage, pagination, deletion, and allowed retention. [Notion custom MCP client](https://developers.notion.com/guides/mcp/build-mcp-client), [Notion connection guide](https://developers.notion.com/guides/mcp/get-started-with-mcp) + +Atlassian documents `https://mcp.atlassian.com/v2/mcp` with interactive OAuth 2.1 and access to Jira, Confluence, and other Atlassian work. Use that source route first. Its separate Jira REST 3LO flow requires a client secret; the REST guide also warns against applications that collect API tokens or instruct customers to create individual 3LO apps. Do not use the current Google own-client setup as a general template for Jira. Tenant callback allowlists can still block a desktop sign-in. Do not assume this MCP includes Trello. [Atlassian MCP setup](https://developer.atlassian.com/cloud/rovo-mcp/guides/getting-started/), [MCP OAuth](https://developer.atlassian.com/cloud/rovo-mcp/guides/configuring-oauth-2-1/), [Jira REST authorization](https://developer.atlassian.com/cloud/jira/platform/oauth-2-3lo-apps/) + +Google recommends the non-sensitive `drive.file` scope for files the user opens or shares with the application. It does not grant a complete Drive account view. Full-drive read access uses a restricted scope. Pick the product behavior before choosing the scope; a selected-file reader should not be called whole-drive sync. Native Google documents also need an export/read route rather than a normal binary download assumption. [Drive scopes](https://developers.google.com/workspace/drive/api/guides/api-specific-auth) + +Dropbox documents PKCE for public clients such as Desktop and mobile apps. It fits the direct-on-device rule without embedding a shared confidential secret. Resolve the file scope, app approval, change cursors, and content parser behavior as part of implementation. [Dropbox authorization](https://docs.dropboxapi.com/dropbox-api/docs/get-started/authorization) + +Trello documents a public API key and a private user token obtained through delegated authorization. The key alone does not expose user data. Off Grid should register its application and let the user authorize it; users should not create their own developer application. Test safe callback delivery on each platform and keep tokens out of URL logs. Use periodic reconciliation rather than requiring a public webhook receiver on the user's device. [Trello API introduction](https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/) + +### Can social accounts provide one complete view? + +Some services expose a user's feed, but there is no verified universal API for everything a user can see in every social app. Separate home feed, own posts, mentions, bookmarks, notifications, and direct messages in the capability model. Access to one does not establish access to the others. An MCP wrapper cannot supply data that its underlying provider denies. + +| Social source | Verified route or limit | Product decision | +| --- | --- | --- | +| Mastodon | Timeline APIs include authenticated user timelines | Good first social feasibility target; test the selected instance's rules | +| Bluesky | Official protocol defines the requesting account's home-timeline query with pagination | Good API candidate; authentication, chat, notifications, and retention need separate checks | +| X | Official API exposes authenticated reverse-chronological home timeline and mentions | Feasible for these features; check access cost and developer terms before release; this is not the algorithmic feed | +| LinkedIn | Member feed management requires approved Community Management access | Approval-dependent; do not promise normal sign-in grants full home feed or inbox | +| Instagram | Meta's published API collection describes professional-account access and excludes consumer accounts for Facebook Login | Do not promise a general personal-feed aggregator; distinguish professional tools from consumer access | +| Reddit | Data API use is approval-dependent, including commercial eligibility | Resolve approved product use before prioritizing a consumer aggregator | + +Mastodon's API describes user timeline access. Bluesky's official lexicon defines `app.bsky.feed.getTimeline`. X documents a home timeline from followed accounts that excludes algorithmic ranking and requires user authentication. These are concrete feed capabilities, not proof of a complete cross-service inbox. [Mastodon timelines](https://docs.joinmastodon.org/methods/timelines/), [Bluesky timeline definition](https://raw.githubusercontent.com/bluesky-social/atproto/main/lexicons/app/bsky/feed/getTimeline.json), [X timelines](https://docs.x.com/x-api/posts/timelines/introduction) + +LinkedIn's member feed program requires approval. Meta's published Instagram collection describes professional-account restrictions; direct Meta developer pages remained partly inaccessible in this research. Reddit says approved use and commercial eligibility depend on app review. Consumer Facebook feed and inbox access was not verified here. Keep these as explicit limits or questions rather than unsupported complete-account promises. [LinkedIn access](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/community-management-api-migration-guide?view=li-lms-2026-03), [Meta Instagram API collection](https://www.postman.com/meta/instagram/documentation/6yqw8pt/instagram-api?entity=request-23987686-15c537cd-a773-4b40-a87c-27829e49a439), [Reddit access rules](https://support.reddithelp.com/hc/en-us/articles/14945211791892-Developer-Platform-Accessing-Reddit-Data) + +Recommendation: deliver work context first. If social demand is strong, start a separate Mastodon/Bluesky experiment, then evaluate X. Present a combined view of the capabilities actually granted. Do not market it as every social feed and message in one place. No social accounts were connected for this research. + +## Updated decision: one MCP interface across platforms + +The product requirement is now a uniform MCP-based connection system across supported platforms and form factors. This changes the first-release order below. Mac EventKit remains an optional local adapter; it is not the common connection foundation. The checked Desktop build configuration targets macOS, Windows, and Linux. Other form factors were not inspected in this repository. The following mobile design is a proposal, not a claim of existing support. + +**Recommended design:** bundle Off Grid-owned MCP adapters with the app. Each adapter calls the provider directly. Expose the same tool definitions, account identity, permission status, pagination, and result shapes to every Off Grid client. Reuse the existing Google REST implementation behind its MCP adapter. A service does not need to offer its own MCP endpoint for this to work. + +The data path is `Off Grid client → on-device MCP adapter → source provider`. There is no hosted Off Grid connector. Source-operated MCP can also be used where supported, but it must pass the same account, action, and retention checks. A shared MCP interface does not make provider authorization or data-storage rules uniform. + +Share the tool contract and provider logic where the runtime permits it. On Desktop, a bundled adapter can use the Electron runtime and an MCP stdio transport. Do not require a separate Node installation. On a mobile platform that cannot launch this process, embed the adapter and use a documented in-process MCP transport through that platform's SDK. Do not promise that the Desktop Node process can run unchanged on mobile. MCP permits custom transports that preserve its protocol semantics. [MCP transport specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports) + +Use platform-specific browser callbacks and protected credential storage behind common interfaces. Authenticate each device directly unless a provider-approved credential transfer is explicitly designed. Share provider configuration and data schemas; do not assume a Desktop loopback OAuth callback works in every form factor. Browser-only deployment needs a separate check of provider CORS and public-client rules. It cannot fall back to a hosted connector under this requirement. + +Define a small common tool set: search messages, read a message or thread, list calendars, and list events. Add sending later through the existing action controls. Preserve provider-specific IDs and capabilities rather than pretending every service has the same features. Keep a separate resumable sync interface in the adapter: MCP tool access alone does not maintain a complete mailbox. A provider capability record must say whether local retention is allowed, unresolved, or prohibited. + +### Priority: least effort and highest useful reach + +These are relative engineering estimates, not measured development times or user-market statistics. The order weighs existing code, supported provider routes, setup friction, and the number of services covered. Registration and approval effort is shown separately because a small adapter can still have a long release dependency. + +| Priority | Connection | Adapter effort | Release dependency | Expected impact and reason | +| --- | --- | --- | --- | --- | +| 0 | Shared bundled MCP adapter host and contract | Low to medium on Desktop; unmeasured on other form factors | Verify runtime and OAuth callback support on each target | Required once; gives every later provider the same client interface | +| 1 | Gmail and Google Calendar | Low for initial MCP tools; medium for complete sync | Google verification for default consumer sign-in | Highest reuse: existing direct REST, OAuth, and tool hooks already work as the base | +| 2 | Outlook.com and Microsoft 365 mail and calendars | Medium | App registration, publisher verification, and tenant consent | One Graph adapter supplies two major services through normal sign-in | +| 3 | Shared IMAP and CalDAV adapters | Medium | Provider credential and compatibility checks | Broad provider coverage: iCloud, Yahoo, Zoho, custom mail, and compatible calendar servers; setup is less simple than OAuth | +| 4 | Slack live tools through official source MCP | Low to medium client integration; medium overall | Published or internal app eligibility; confirm public-client flow and retention | Strong work-chat value with an existing source MCP endpoint; do not connect restricted search output to automatic memory import | +| 5 | Fastmail JMAP adapter | Low to medium after the common host | Provider OAuth registration; separate calendar credentials | Cleaner mail sync for one provider; IMAP already supplies a fallback | +| Hold | Telegram | High native packaging and lifecycle work | Resolve AI-use and participant-consent rules | A usable client library exists, but the unresolved policy blocks general AI import | +| Hold | Personal WhatsApp and WeChat | High and uncertain | No verified supported general inbox route in this research | Do not make unsupported client maintenance the foundation of the uniform system | + +Slack's source server requires a registered, published or internal application; a generic MCP URL alone is not sufficient. Gmail's source MCP is still a restricted Developer Preview. These facts favor a bundled Google adapter and a conditional official Slack adapter. [Slack MCP eligibility](https://docs.slack.dev/ai/slack-mcp-server/), [Gmail MCP preview](https://developers.google.cn/workspace/gmail/api/guides/configure-mcp-server?hl=en) + +**First concrete deliverable:** wrap the current Google provider in the common MCP contract and prove the same search and event calls on macOS, Windows, and Linux. Then add Graph without changing the client-facing contract. Validate another form factor's embedded runtime before claiming full portability. EventKit can later expose the same calendar tools on Mac, but must remain an optional capability rather than a requirement for all clients. + +The sections below retain the detailed provider evidence. Their earlier Mac-first order addressed minimum setup on this Desktop app; the updated MCP-first order above controls the current recommendation. + +Off Grid can connect directly to email, calendar, and some messaging services. The best general design is a provider module in the Electron main process. That module calls the source service and supplies tools to the existing connection system. The proposed design keeps credentials, imported data, and AI processing on the device. + +The first work should reuse the existing Mac calendar reader, add Microsoft mail and calendar access through Microsoft Graph, and reduce Google setup steps with a registered desktop OAuth application. Slack is feasible, but its publication and data storage rules affect the design. Telegram is technically feasible but has a material AI use restriction. Personal WhatsApp and WeChat do not have a verified general inbox API that meets the requirements in this research. + +Recommendations in this report are engineering judgments. They are not results from live account tests. Provider facts have links to their sources. Proposed setup steps and implementation choices are identified as proposals. + +## Scope and direct connection rule + +The requirement is interpreted as follows: Off Grid may contact the user's source service, such as Google or Microsoft, from the device. It must not send data through an Off Grid server, a connector service, or a cloud AI service. Without contact with the source service, the app can read only data already available on the device. + +Three paths meet this interpretation: + +1. Off Grid calls the provider's API directly. +2. Off Grid calls a local operating system API or a local provider program. +3. Off Grid calls an MCP endpoint operated by the source provider, if its terms permit the use. + +A third-party hosted MCP server does not meet this rule. A local MCP program may meet it, but only if its actual network requests go directly to the source. A local process can still send data elsewhere. MCP does not establish the data path or remove provider permission requirements. The protocol supports both local standard input/output and HTTP transports. [MCP transport specification](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports) + +For the default consumer product, prefer a built-in provider module over a program that the user must install with Node, Python, Docker, or a terminal. Keep custom local MCP connections as an advanced option. + +This report covers reading data, searching it, keeping a local copy where permitted, and later communication actions. It distinguishes these tasks from letting a user send commands to Off Grid through a bot. A bot chat does not usually grant access to the user's full inbox. + +## Existing code and what to reuse + +The managed worktree uses core commit `9a5181f3a3c5a2eb9b5ce637cc0ee655a8d703a8` and Pro commit `08531ba6521de1b6b3e257ece01269cde88bc585`. It starts from the repository's remote default branch. The Google client and REST files were also compared with the original checkout. Those files had no differences. No application code was changed. + +| Existing file | Observed behavior | Effect on the proposal | +| --- | --- | --- | +| [Google client](../../pro/main/google-client.ts) | Each user supplies a Google Web application client ID and secret. One client serves Gmail and Calendar. | Reuse the direct data path. Do not make this setup the default for every new service. | +| [Google REST module](../../pro/main/google-rest.ts) | Calls Gmail v1 and Calendar v3 directly. Refreshes and retries once after HTTP 401. | This is the main pattern for new provider modules. | +| [Provider registration](../../pro/main/index.ts) | Registers `HOOKS.mcpConnectorToolSource` for Google tools. | Provider-owned tools can use REST without running an MCP server. | +| [Core connection system](../../src/main/mcp.ts) | Stores connection records. Supports HTTP and local processes. A `ConnectorToolSource` owns tool definitions, verification, and execution. | Keep this common interface. Provider implementations belong in Pro. | +| [OAuth provider](../../src/main/mcp-oauth.ts) | Uses the system browser, state, PKCE, and a local callback. Static client configuration currently requires a secret. | Generalize static public-client configuration for Microsoft, Slack, and other native clients. | +| [OAuth callback](../../src/shared/mcp-oauth-callback.ts) | Uses `http://127.0.0.1:33418/callback`. | Keep existing Google Web clients working. Add provider-specific native redirect configuration. | +| [Secret store](../../src/main/secrets.ts) | Stores encrypted blobs in SQLite. Electron `safeStorage` uses an OS-held encryption key. | Reuse it. Credentials are not each stored as a separate Keychain entry. | +| [Memory import](../../pro/main/ingest.ts) | Imports Google data and has a Slack adapter for named MCP tools. | Add explicit provider import methods. Do not infer all sync behavior from arbitrary tool names. | +| [Service timer](../../pro/main/services.ts) | Starts connection refresh after about one minute and repeats every 30 minutes. | Add per-provider cursors and schedules. Retain a visible last successful sync time. | +| [Native actions helper](../../scripts/actions-helper/main.swift) | Already reads calendar events with EventKit, creates events, and sends mail with AppleScript. | Reuse the existing native boundary for Mac calendars. Mail reading still needs separate work. | +| [Native action mapping](../../src/main/actions/semantic-rail.ts) | Maps calendar lookup to `calendar.listEvents`. | A native calendar connection does not require a second standalone helper. | +| [Action control](../../src/main/actions/chat-connector-action.ts) | Routes chat connection mutations through the durable action engine. | Keep sending and calendar changes on this route. | + +The present Google tools are narrower than a full email or calendar client. Gmail search returns up to 20 messages, with headers, snippets, and links. It does not return full message bodies or attachments. Calendar reads the primary calendar, with a default 14-day range and a result limit of 50. These functions do not follow result pagination or maintain change cursors. + +The Google memory import turns these results into observations. A richer connection needs stable source IDs, update handling, deletion handling, and a complete account identity. Repeatedly importing a short recent list is not sufficient to maintain a complete local mailbox. + +There is also an existing Slack import. It looks for `slack_get_channel_history`, resolves users, selects channels where the bot is a member, reads up to 12 channels, and stops after 50 messages. It is a bot-oriented path. It is not a complete personal Slack connection, and the official Slack MCP tool set must be mapped explicitly. + +Two current behaviors need attention before broader messaging import. The importer logs the start of generic tool results and a sample Slack history result. These logs can contain message data. The service timer also runs connection sync before it checks whether screen capture is paused. Capture pause therefore does not pause connection import. Add a clear per-account sync control, and decide how a global pause should affect it. Remove content samples from normal logs before enabling sources that prohibit retention. + +## Service comparison + +The setup ratings below are estimates for the proposed product. Low means normal sign-in or one OS permission. Medium means an app password, local program, or workspace approval. High means developer setup or an unsupported protocol. The detailed sections contain the source evidence and conditions. + +| Service | Preferred direct path | Consumer setup | Main limit | Recommendation | +| --- | --- | --- | --- | --- | +| Calendars already on the Mac | Existing EventKit helper | Low if accounts are already configured | Mac only; OS controls refresh | Start here | +| Gmail | Direct Gmail API and desktop OAuth | Low after Off Grid verification; high with current own-client setup | Restricted scopes and verification | Keep REST; improve sign-in | +| Google Calendar | Direct Calendar API; EventKit as a Mac option | Low after application setup | Calendar selection and complete sync need work | Expand existing module | +| Outlook.com and Microsoft 365 mail | Microsoft Graph and public-client OAuth | Low for personal accounts; may need work administrator approval | Organization consent policy | First new network provider | +| Microsoft calendars | Same Graph account; EventKit when configured on Mac | Low to medium | Shared calendar and tenant limits | Add with Microsoft mail | +| Fastmail | JMAP OAuth for mail; CalDAV for calendars | Low after provider registration; medium with a token | Calendar credentials need separate verification | Good second email provider | +| iCloud Mail | IMAP and SMTP with an app password | Medium | New Apple account authorization is not yet verified for Off Grid | Use standards fallback | +| Yahoo and AOL | IMAP and SMTP with an app password | Medium | Password availability and provider OAuth approval | Add provider presets | +| Zoho and other IMAP mail | IMAP; provider REST when useful | Medium | Plan, region, and administrator settings | Support through shared mail adapter | +| Proton Mail | Local Proton Mail Bridge and IMAP | Medium | Paid plan and Bridge installation | Optional local integration | +| Other calendars | CalDAV or a selected calendar feed | Medium | Feed may be read-only or delayed | Shared calendar adapter | +| Slack | Source-operated MCP or direct API with user OAuth | Low after publication and workspace approval | MCP eligibility, rate limits, and retention rules | Feasibility work before release | +| Telegram | TDLib as a user client | Low to medium technically | AI use and participant consent terms | Hold general AI import | +| Personal WhatsApp | No verified official general inbox path; local linked-device libraries exist | QR setup can be simple | Unsupported clients, account blocks, incomplete history | Optional research only | +| WhatsApp Business | Direct Meta API | High for consumers | Business setup and inbound delivery design | Separate business product | +| WeChat personal chats | No verified supported inbox route found | High or uncertain | Web login eligibility and unavailable official evidence | Defer automatic sync | +| Teams work chats | Microsoft Graph | Medium | Work accounts and broader permissions | Later Microsoft extension | +| Matrix | Direct client-server API | Medium | Encryption keys and homeserver policy | Good open-protocol extension | +| Signal | Local `signal-cli` | Medium | Community client and frequent maintenance | Experimental extension | +| Discord | Official bot API | Medium | Bot access is not personal inbox access | Limit to bot-visible spaces | + +## Google mail and calendars + +### Preserve the direct REST path + +The existing Google data path fits the requirement. Keep it as the production basis. The Gmail MCP guide still identifies the server as a Developer Preview and requires program membership. MCP also adds service setup that is not needed for the current REST functions. [Gmail MCP configuration](https://developers.google.cn/workspace/gmail/api/guides/configure-mcp-server?hl=en) + +### Reduce setup through a desktop application + +Proposed default: Off Grid registers and verifies its own Google Desktop application. The user selects Connect, signs in through the system browser, and grants the selected service access. Authorization code exchange and token refresh occur on the device. + +Google supports PKCE and a local loopback redirect for desktop clients. A distributed native client cannot keep an application secret confidential. An application registration identifies Off Grid to Google; it does not require an Off Grid data server. Google also says installed applications do not support incremental authorization. Design mail and calendar grants with that limit in mind. [Google desktop OAuth](https://developers.google.com/identity/protocols/oauth2/native-app) + +Retain the user's own client as an advanced option. Do not silently convert an existing Web application registration to Desktop type. Its callback rules and saved tokens belong to the existing client. + +Current own-client setup has a recurring friction point: an External consent screen in Testing issues refresh tokens that normally expire after seven days when mail or calendar scopes are requested. This can cause frequent reconnects. It is a provider behavior, not necessarily a token storage defect. [Google token expiry rules](https://developers.google.com/identity/protocols/oauth2#expiration) + +### Verification and access level + +`gmail.readonly` is a restricted scope. `gmail.metadata` is also restricted; changing to metadata does not remove restricted-scope verification. `gmail.send` is a separate sensitive scope. Request it only when sending is added. [Gmail scopes](https://developers.google.com/workspace/gmail/api/auth/scopes) The metadata scope also does not support Gmail's `q` search parameter. [Gmail message search parameters](https://developers.google.com/workspace/gmail/api/reference/rest/v1/users.messages/list) + +Google's restricted-scope guide requires an assessment when the application can access restricted data from or through a third-party server. Local processing is relevant to this assessment, but it is not a promise of approval or exemption. Document the complete data path, including optional remote models, sync, crash reports, and backups, before making a verification claim. Confirm the assessment decision with Google's verification team. [Restricted-scope verification](https://developers.google.cn/identity/protocols/oauth2/production-readiness/restricted-scope-verification?hl=en) + +Google lists email productivity features, including generative AI summaries, as an approved Gmail use case. Its data policy also requires clear disclosure, limited access, secure handling, and deletion support. Do not use imported mail to train a general model. [Google Workspace data policy](https://developers.google.com/workspace/workspace-api-user-data-developer-policy) + +For Calendar, request only the event and calendar-list scopes needed by the released features. The current module asks for free/busy access as well. Review whether the current REST tool uses it before carrying that request into a new application registration. [Calendar scope reference](https://developers.google.com/workspace/calendar/api/auth) + +### Complete the sync model + +Proposed next steps are full bodies on demand, selected folders or labels, pagination, and a stable source record before summary generation. Keep Gmail's `historyId` for partial sync. If the history cursor is no longer valid, perform a bounded full sync. [Gmail synchronization](https://developers.google.com/workspace/gmail/api/guides/sync) + +For Calendar, add calendar selection and per-calendar sync tokens. Process deleted events and invalid token recovery. The current moving date window must not simply be combined with a sync token; use request parameters that the API permits and a stable sync query. [Calendar synchronization](https://developers.google.com/workspace/calendar/api/guides/sync) + +## Microsoft email and calendars + +Outlook is an application and product name. Microsoft-hosted Outlook.com, Hotmail, Live, and Microsoft 365 mail can use one Graph provider. A Gmail account shown inside Outlook still belongs to Google. An on-premises Exchange mailbox is a separate case. + +Microsoft supports authorization code with PKCE for desktop public clients and supports both personal and work or school accounts. Register an Off Grid application for the required account types. A public client does not need a shared application secret. [Microsoft authentication flows](https://learn.microsoft.com/en-us/entra/identity-platform/authentication-flows-app-scenarios) + +Proposed implementation: use `@azure/msal-node` in the main process for sign-in and token management. Microsoft supplies an Electron system-browser example. Store the MSAL token cache with the existing protected storage. Use silent token acquisition after the initial sign-in. Do not copy the example's plaintext file cache into the product. [Microsoft Electron example](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/samples/msal-node-samples/ElectronSystemBrowserTestApp/README.md), [MSAL token acquisition](https://learn.microsoft.com/en-us/entra/msal/javascript/node/acquire-token-requests) + +Use delegated permissions. For the present Gmail-equivalent experience, use `Mail.Read` because `Mail.ReadBasic` excludes the body and preview. `Calendars.ReadBasic` can cover basic event details; use `Calendars.Read` if the released feature needs event bodies. Add `Mail.Send` or `Calendars.ReadWrite` only with the relevant action. `Mail.ReadWrite` does not itself grant sending. Shared resources have separate permissions and account limits. [Graph permission reference](https://learn.microsoft.com/en-us/graph/permissions-reference) + +Proposed initial endpoints are `/v1.0/me/messages`, `/v1.0/me/mailFolders`, `/v1.0/me/calendars`, and a bounded calendar view. Use API pagination and per-resource change cursors. Calendar delta tracks a defined calendar view; retain its date range as part of the cursor state. [Calendar delta](https://learn.microsoft.com/en-us/graph/api/event-delta?view=graph-rest-1.0) + +Microsoft publisher verification can reduce consent friction. It cannot override an organization's policy. A tenant can require administrator approval even for delegated permissions that do not normally require it. Show that state clearly and retain the completed local setup. Do not ask the user to repeat sign-in without a change in policy. [Publisher verification](https://learn.microsoft.com/en-us/entra/identity-platform/publisher-verification-overview) + +Do not build a new Exchange Online connection around EWS. Microsoft's current guidance says phased disablement starts on 1 October 2026 and permanent retirement is on 1 April 2027. This does not mean on-premises Exchange has the same retirement date. [Microsoft EWS retirement guidance](https://learn.microsoft.com/en-us/skypeforbusiness/hybrid/prepare-for-ews-retirement) + +Proposed user steps: select Microsoft, sign in, choose Mail and/or Calendar, then choose the import scope. Users should not need to create an Azure application. Keep an own-client option for organizations that require it. Never borrow Microsoft Office or another application's client ID to avoid registration. + +## Mac calendars and other calendar services + +### Reuse EventKit first + +The native helper already calls `requestFullAccessToEvents` on macOS 14 and later, then reads events in a specified date range. Older macOS uses the earlier request method. The app build configuration already contains calendar permission descriptions. + +Apple's Calendar supports iCloud, Google, CalDAV, and Exchange accounts. Thus, a local EventKit connection can cover several providers when the account is already configured on the Mac. It reads the OS calendar store. Off Grid does not need the provider's password or OAuth token. [Mac calendar accounts](https://support.apple.com/guide/calendar/add-calendars-and-calendar-accounts-icl4308d6701/mac) + +Reading existing events requires full calendar access under the newer permission model. This permission also allows writing; the OS does not supply an equivalent read-only event grant. Keep Off Grid's default tools read-only even though the OS permission is broader. [Apple calendar access levels](https://developer.apple.com/documentation/technotes/tn3152-migrating-to-the-latest-calendar-access-levels) + +Proposed work: extend the existing helper to enumerate calendar IDs and sources, accept a calendar selection, and return source identifiers, time zones, locations, and attendees where available. Use the current helper invocation boundary. Verify permission attribution in the signed app when implementation begins. + +The existing helper is a one-shot process. It cannot retain `EKEventStoreChanged` subscriptions after it exits. Begin with a small periodic local read. Add a persistent native component only if update speed requires it. EventKit supplies store-change notifications for a running client. [EventKit access guide](https://developer.apple.com/documentation/eventkit/accessing-calendar-using-eventkit-and-eventkitui?changes=_7) + +Limit: Off Grid cannot promise a provider refresh time from a local store read. macOS owns account sync. If the user has not added an account, adding it through Calendar or Internet Accounts is an extra step. Direct Google or Graph access remains useful for those users. + +### CalDAV and calendar feeds + +Proposed second path: one CalDAV adapter for providers such as Fastmail and compatible self-hosted services. Use discovery, selected calendars, event UIDs, recurrence IDs, and ETags. Detect supported change-sync features rather than assuming every server has them. `tsdav` is a candidate TypeScript client; it needs a provider compatibility check before adoption. [tsdav documentation](https://tsdav.vercel.app/docs/) + +An ICS file import is a simple offline fallback. A subscribed calendar URL is a read-only network source and can update slowly. Treat a private feed URL as a credential. Do not present it as a full calendar account connection. Proton offers calendar sharing by link; that does not establish a CalDAV account API. [Proton calendar sharing](https://proton.me/support/share-calendar-via-link) + +Do not import the same calendar twice through EventKit and a direct API. Let the user select one source route. Matching an event by title and time alone is not a safe general deduplication rule. + +## Other email providers + +### Shared IMAP and SMTP support + +Build a common direct mail adapter. IMAP handles reading and folder state. SMTP handles sending. Add provider presets and an advanced server form. Use TLS and normal certificate validation. A provider domain or MX record alone is not enough to identify all server and authentication settings safely. + +`ImapFlow` is a candidate for direct IMAP access. It supports message retrieval, searching, mailbox management, and IMAP extensions. Use the library without deploying the separate EmailEngine service. [ImapFlow](https://imapflow.com/) + +For proposed sync state, keep mailbox identity, `UIDVALIDITY`, UIDs, and the server's supported modification state. Use read operations that do not mark mail as read. Use IDLE while the app runs where supported, with reconnect and periodic reconciliation. Do not keep every account connection active without a resource budget. + +`Nodemailer` is a candidate for SMTP. Keep protocol debug logging disabled for customer data. For attachments, use validated local inputs and disable arbitrary file or URL access. Sending must use the app's action controls. [Nodemailer SMTP](https://nodemailer.com/smtp) + +### Fastmail + +Fastmail documents JMAP mail access, OAuth with PKCE, loopback redirects, API tokens, and CalDAV calendar access. Register Off Grid for the consumer OAuth path. An API token is a useful advanced fallback. The OAuth documentation does not prove that the resulting JMAP token can also authenticate CalDAV; verify that separately. [Fastmail developer documentation](https://www.fastmail.com/dev/) + +Recommendation: use JMAP for Fastmail mail instead of IMAP if this provider is built as a dedicated module. Its structured API fits a local account and change-state model. Do not assume Fastmail calendars have the same production JMAP interface as its mail. Use CalDAV for the documented calendar route. + +### iCloud Mail + +An app-specific password is the verified fallback for IMAP and SMTP. It requires two-factor authentication on the Apple Account. The user creates a password for Off Grid and can revoke it. Do not request the main Apple Account password. [Apple app-specific passwords](https://support.apple.com/en-us/102654) + +Apple now also documents account authorization for supported third-party Mail, Calendar, and Contacts apps. It is therefore inaccurate to say all iCloud access always requires an app password. However, that support page does not establish a public registration process that Off Grid can use today. Treat direct Apple account authorization as a provider inquiry, not a ready implementation. Generic Sign in with Apple is not evidence of mail access. [Apple third-party account authorization](https://support.apple.com/en-us/121539) + +For iCloud calendars on Mac, prefer EventKit. It avoids a second credential setup when the account is already configured. + +### Yahoo and AOL + +Use the shared mail adapter with an app-password setup guide. Yahoo documents that third-party apps with older sign-in methods need an app password. Availability and account restrictions must be checked in the product's feasibility test. [Yahoo secure access](https://uk.help.yahoo.com/kb/SLN27791.html) + +Yahoo also documents an OAuth registration with a consumer key and secret. Do not assume this server-oriented flow can be shipped as a secret-free native flow, or that mail access is automatically approved for a new application. Resolve these questions before promising one-click Yahoo sign-in. [Yahoo OAuth flow](https://developer.yahoo.com/oauth2/guide/flows_authcode/) + +### Zoho and custom domains + +Zoho documents both REST OAuth and IMAP access. Its IMAP help describes account settings and plan limits. Use the shared adapter first, with the correct regional server and a clear explanation if IMAP is disabled. Add REST only if it materially reduces setup or supplies a required capability. [Zoho IMAP](https://www.zoho.com/mail/help/imap-access.html), [Zoho OAuth](https://www.zoho.com/mail/help/api/using-oauth-2.html) + +### Proton Mail + +Proton Mail Bridge provides local IMAP and SMTP to desktop clients. It requires a paid Proton Mail plan. Off Grid can connect to Bridge on the same device; Bridge handles the Proton connection and mail decryption. The user must install and sign in to Bridge and enter its local connection credentials. [Proton Bridge setup](https://proton.me/support/imap-smtp-and-pop3-setup) + +Keep Bridge's local credentials protected. Do not expose the local mail port on the network. Do not assume Mail Bridge also supplies a Calendar API. Use a separate calendar path if needed. + +### Apple Mail as a local source + +The native helper already sends mail through AppleScript, but it does not read mail. A selected-mail import or bounded Apple Mail reader is a reasonable feasibility experiment for users who already have accounts configured. + +This is a proposal, not verified inbox support. Check the Mail scripting dictionary, Automation permission, retrieval performance, attachments, and whether requested bodies are already downloaded. Prefer supported scripting over reading the Mail app's private database. Do not copy another app's account tokens. Do not require Full Disk Access merely to avoid a provider sign-in flow. + +## Slack + +### Direct desktop sign-in is now supported + +Slack's current PKCE documentation supports desktop public clients without a client secret. Desktop redirects may request user scopes, not bot scopes. Custom schemes require PKCE. The documented public-client flow also changes token rotation behavior, and PKCE refresh tokens expire after 30 days. The product must handle that expiry and concurrent refresh safely. [Slack PKCE](https://docs.slack.dev/authentication/using-pkce/) + +This changes the recommendation from older Slack integrations. An Off Grid-owned public application can remove developer credential setup for the user. It still requires registration, correct scopes, and workspace permission. The dedicated user token exchange is documented for MCP clients. [Slack user token exchange](https://docs.slack.dev/reference/methods/oauth.v2.user.access/) + +### Official MCP is a direct source option + +Slack supplies `https://mcp.slack.com/mcp` over Streamable HTTP. It does not support dynamic client registration. Only published directory applications and internal applications may use it; unlisted distributed applications are prohibited. Its server offers search, message reading, and actions. This is source-operated MCP, so it can fit the direct connection rule. [Slack MCP overview](https://docs.slack.dev/ai/slack-mcp-server/) + +Proposal: test the official MCP path first for live chat tools, using an internal test application. For consumer distribution, resolve Marketplace eligibility and publication. Configure a fixed public client and the correct user OAuth endpoints. Do not use the current generic dynamic-registration assumption unchanged. + +### Live search and saved memory need different designs + +Slack's Real-time Search API supports user queries from outside Slack through a user token. It restricts access to published or internal applications. It prohibits storing or copying retrieved data, training use, and scraping unrelated to user queries. It also requires an in-Slack experience under its general usage guidelines. [Slack Real-time Search](https://docs.slack.dev/apis/web-api/real-time-search-api/) + +Recommendation: keep this route for live, user-requested context. Do not send these results to `recordObservation`, embeddings, persistent model history, or paired-device sync. A local disk copy is still a copy. Confirm how these rules apply to official MCP search results and derived summaries before retention is enabled. + +For a separate Conversations API import, confirm the applicable Slack agreement and approved product use. Do not infer a right to store an entire workspace from possession of an OAuth token. The current Slack memory import must not automatically run for a new official MCP connection. + +### History limits and bot alternatives + +`conversations.history` has a limit of one request per minute and 15 objects for affected new commercially distributed non-Marketplace applications or installations. Marketplace and internal applications have different limits. User tokens and bot tokens also have different conversation visibility. [Slack history method](https://docs.slack.dev/reference/methods/conversations.history/) + +At 15 messages per minute, 10,000 messages require about 667 minutes, or 11.1 hours, before retries and extra thread requests. This is a calculated lower-bound example, not a benchmark. It makes full history import a poor default for that application class. Check thread limits independently. MCP must not be treated as a way to evade API limits. + +Socket Mode receives events through an outbound WebSocket without a public callback. However, it needs an app-level token, and Socket Mode applications cannot currently be listed in the public Marketplace. It is useful for a user's own internal app, not a shared app-level secret embedded in every consumer installation. [Slack Socket Mode](https://docs.slack.dev/apis/events-api/using-socket-mode/) + +Recommended Slack order: direct live search and selected-thread reads, then approved communication actions, then a separately approved retained-memory design if Slack permits it. + +## Telegram + +TDLib is Telegram's official client library. It handles networking, encryption, and local storage. It is a better base for a user-account connection than a bot API wrapper. [Telegram TDLib](https://core.telegram.org/tdlib) + +QR authentication is available through TDLib. It can reduce setup steps when the user already has Telegram on a phone. The login state machine must still handle password and other account challenges. Off Grid needs its own application API identity. A user should not need to create a developer application for the default product. [TDLib QR authentication](https://core.telegram.org/tdlib/docs/classtd_1_1td__api_1_1request_qr_code_authentication.html), [Telegram application registration](https://core.telegram.org/api/obtaining_api_id?source=post_page---------------------------) + +The material limit is policy. Telegram's content terms prohibit broad scraping, indexing, aggregation, and use for AI development or deployment. The stated exception requires explicit, informed, affirmative, continued consent from all relevant users for the specific content or chat context. One account holder's connection consent does not establish that condition. [Telegram content terms](https://telegram.org/tos/content-licensing) + +Recommendation: hold general AI inbox ingestion until Telegram clarifies the intended use and consent process. A non-AI client capability and a scoped conversation where the required consent exists are different product cases. Local inference alone does not remove the restriction. + +If this is resolved, run TDLib as a managed local component and keep sessions and keys protected. It needs a persistent lifecycle, unlike the current on-demand MCP process. Do not assume cloud chat history also grants access to secret-chat history on another device. A Telegram bot can provide a command interface to Off Grid, but cannot substitute for the user's inbox. + +## WhatsApp + +### Personal inbox and agent chats are different + +WhatsApp now documents third-party agents. They can read only what the user shares with them, cannot join or access other chats, and are available only in limited countries. This is useful for an Off Grid command interface, but not for personal inbox access. [WhatsApp third-party agent help](https://faq.whatsapp.com/1050934623978152) + +The agent terms were updated on 25 August 2026. They state that agent conversations are not end-to-end encrypted personal conversations and use Meta's agent platform. These terms do not establish a device-only transport or a general personal inbox API. [WhatsApp agent terms](https://www.whatsapp.com/legal/third-party-agents-terms) + +### Local linked-device libraries are technically possible + +Baileys is a TypeScript library that uses a direct WebSocket connection. It supports QR or pairing-code authentication as another linked client and does not need a browser. It is not an official WhatsApp integration. [Baileys project documentation](https://raw.githubusercontent.com/WhiskeySockets/Baileys/master/README.md) + +`whatsapp-web.js` uses Puppeteer and a managed browser to access WhatsApp Web internals. Its project warns that account blocking remains possible. Packaging a second browser also adds a resource and maintenance burden to this Electron app. [whatsapp-web.js project documentation](https://raw.githubusercontent.com/pedroslopez/whatsapp-web.js/main/README.md) + +Engineering judgment: if Off Grid chooses to fund an experimental personal WhatsApp connection, evaluate Baileys first. It offers the simpler local architecture. The experiment must prove reconnect behavior, session key storage, history completeness, message edits, deletion, disappearing messages, and account stability. It must not claim provider support or complete history access. + +A local MCP wrapper does not change those limits. It only exposes the same unofficial client through tools. Do not use a managed cloud bridge to reduce setup steps under this task's direct connection requirement. + +Recommendation for the normal product: offer deliberate local import of selected user-provided messages or exports. Keep continuous personal WhatsApp sync outside the first release. This is a product reliability decision; the research did not establish that a direct local implementation is impossible. + +### Business API and new business MCP + +WhatsApp Business API access serves a business account, not arbitrary access to a consumer account. A direct implementation must resolve business registration, phone number setup, permissions, and inbound message delivery. Receiving webhooks on a consumer Mac behind a router is a substantial obstacle under a strict no-relay rule. An external tunnel would add another service to the data path. + +Meta's developer overview required login or returned rate-limit errors during this research. Its official SDK provides a separate Cloud API setup route. Business account coexistence, current AI-provider restrictions, and the newly reported business-management MCP were not verified from accessible primary documentation. Do not use older blanket claims about WhatsApp AI bans or assume that business-management MCP grants personal inbox access. These remain provider questions. [Meta Cloud API SDK](https://whatsapp.github.io/WhatsApp-Nodejs-SDK/), [Meta developer overview requiring access](https://developers.facebook.com/documentation/business-messaging/whatsapp/overview) + +## WeChat + +No supported general personal WeChat inbox API was verified. Official Account and WeCom documentation could not be read through the research tool. Their product names must not be treated as proof of access to personal WeChat conversations. This is an evidence gap, not a categorical proof that no such capability can exist. + +The local Wechaty Web provider is a real candidate, but its own documentation warns that accounts registered after 2017 may be unable to log in to Web WeChat. It also reports contact and room IDs changing between sessions. Those limits make it unsuitable as a predictable default for new consumers. [Wechaty Web provider](https://wechaty.js.org/docs/puppet-providers/wechat) + +Recommendation: defer automatic personal sync. Investigate supported user export or selected-content import. Evaluate a local UI read only for an explicit user task, using the existing opt-in capture controls and visible indicator. Do not describe screen content as a complete inbox. Do not extract process keys or patch the WeChat application as the default product design. + +Treat WeCom as a separate business request. Before scoping it, verify the archive product, administrator setup, participant consent, IP restrictions, supported SDK platforms, and whether the data can travel directly to the Mac. Do not buy a hosted Wechaty provider unless its data path is acceptable; some provider designs add a remote service. + +## Other communication systems + +Teams can extend the Microsoft provider for work accounts. Graph lists chat messages with delegated `Chat.Read`; personal Microsoft accounts are not supported by that API. Channel messages use different permissions and require separate access work. Mail consent alone does not grant Teams messages. [Graph chat messages](https://learn.microsoft.com/en-us/graph/api/chat-list-messages?view=graph-rest-1.0), [Graph channel messages](https://learn.microsoft.com/en-us/graph/api/channel-list-messages?view=graph-rest-1.0) + +Matrix has a direct client-server protocol for authentication, sync, messages, and device encryption. It fits the proposed architecture well. The main work is a full local client session, including key verification and recovery for encrypted rooms. A Matrix bridge to WhatsApp or WeChat is a separate data path; do not assume that it meets the same requirement. [Matrix client-server specification](https://spec.matrix.org/latest/client-server-api/) + +Signal can use the community `signal-cli` program locally. The project supports linking an existing account and a JSON-RPC daemon. It also warns that releases older than three months may fail after server changes. This adds runtime packaging and maintenance work. Treat it as an optional experiment, not a low-maintenance official provider API. [signal-cli documentation](https://github.com/AsamK/signal-cli/blob/master/README.md) + +Discord supports bot automation. It forbids automating a normal user account as a self-bot outside the supported OAuth or bot API. A bot connection can cover permitted server channels; it does not provide a general copy of the user's private inbox. [Discord self-bot policy](https://support.discord.com/hc/en-us/articles/115002192352-Automated-User-Accounts-Self-Bots) + +## Proposed shared architecture + +Keep provider code and paid memory behavior in `pro/`. Reuse the generic core hooks and tools. Any new core boundary should remain inert when Pro is absent. No Pro implementation should be added to the public core to make a single provider easier. + +Use an explicit provider identifier. Today, a Google connection's MCP URL also selects its REST behavior. A provider ID is clearer for Graph, IMAP, CalDAV, and native sources, which may not have a meaningful MCP URL. + +A proposed provider module needs these operations: + +- Connect an account, cancel an attempt, and report granted access. +- Verify a small authenticated read. +- Enumerate selectable folders, calendars, or conversations. +- Supply tools for live reads and permitted actions. +- Import permitted records with pagination and a resumable cursor. +- Reconcile updates and deletions. +- Revoke access where supported and remove local credentials. +- Report account-specific status without exposing tokens or message bodies in logs. + +Keep account authentication separate from service selection. One Microsoft account can supply Mail and Calendar while the user enables each service separately. Persist a stable provider account ID, tenant where applicable, granted scopes, and connection generation. Do not use a visible display name as the record's identity. + +Keep live search separate from retained memory. Each tool result needs a retention rule supplied by the provider module: transient use only, permitted local retention, or retention not yet established. The memory importer must check this rule. Transient data must also stay out of persistent chat history, tool traces, embeddings, error dumps, and paired-device sync. + +For retained data, store normalized source records before summaries. A proposed identity is provider, account, collection, item ID, and version. Calendar recurrence needs an occurrence identity as well. Keep deleted-record state long enough to remove derived observations. Advance a sync cursor only after records commit successfully. + +Provider modules must report explicit tool risk and capability. The current generic MCP classifier uses the tool name to decide whether a call is a read. That can remain a fallback for advanced custom servers. Built-in modules know whether a call sends, changes, deletes, or only reads. They should use that knowledge. + +Use a per-account sync lock and bounded requests. Apply `Retry-After`, backoff, cancellation, and sleep/wake recovery. Record the last successful sync separately from the last attempt. A denied token, blocked administrator consent, and a temporary rate limit need different status values. + +For direct data retrieval, prefer polling change cursors over adding a public webhook server. Provider-specific outbound streams can reduce delay where available. The current 30-minute timer is a useful starting point, not a universal freshness promise. When the app is closed or the Mac sleeps, a device-only system cannot continue processing on an Off Grid cloud worker. + +Enforce local AI processing for these sources. The fact that a request uses the shared `llm` interface does not alone prove that every configured model route is local. Check the selected execution route before supplying communication data. The same applies to embeddings and summaries. Do not send data to an optional remote model under this research's scope. + +Allow only the provider's required network destinations, including auth, API, and documented content-download hosts. Validate redirect and pagination destinations before forwarding credentials. Do not automatically load remote images in email bodies. Keep message content as data; it must not authorize a new connection or a send operation. + +The current local MCP launcher merges the parent process environment into the child environment. For managed provider programs, pass only the environment values they need. Do not give every local integration all parent environment values. A custom MCP program also needs an explicit installation and trust decision; local execution alone does not prove that its network path meets the requirement. + +Disconnection and deletion are separate user choices. Removing a token does not remove already imported observations. Offer a clear local data removal path, including derived summaries and indexes. Retain the current opt-in and visible indicator requirements for any screen capture fallback. + +## Proposed consumer setup + +The default setup should have four stages: + +1. Select the source. +2. Sign in at the provider or grant the OS permission. +3. Select the services and data scope. +4. Confirm the first small import and show its date range and result count. + +Do not ask a normal consumer for client IDs, client secrets, MCP URLs, terminal commands, or Docker setup. Where a provider requires an app password, show the provider's own page and ask only for the minimum credentials. Keep own-client and custom MCP setup in an advanced path. + +Proposed first import: a bounded recent range and selected folders or calendars. Show older history as an explicit choice with an estimated cost in time or disk space when the provider supports it. This is a product choice, not a claim that 30 days is a provider limit. + +Show the account, selected data, data path, last successful sync, and access that needs renewal. Report a limited local calendar cache or a partial message history plainly. Avoid a green Connected status that implies complete data coverage. + +## Implementation order and decision tests + +### First release work + +1. **Mac calendar connection.** Reuse the EventKit reader. Add calendar selection and a Pro import module. This offers the fewest setup steps for users with configured Mac accounts. +2. **Microsoft mail and calendars.** Register a public application, complete publisher verification, and build a Graph module with protected MSAL caching. This is the strongest new direct network provider. +3. **Google setup improvement.** Keep REST, add a verified Desktop application path, and retain the own-client path. Add selected calendars, paging, and change tracking. +4. **Shared IMAP and CalDAV.** Add iCloud, Yahoo, Zoho, and custom-provider presets. Add Proton Bridge as a named local option. +5. **Fastmail.** Add JMAP OAuth if demand justifies a dedicated mail module. + +### Separate feasibility work + +- **Slack:** prove public-client login, refresh, MCP eligibility, account visibility, and live search without persistent copies. Resolve approved retained-memory use before connecting the autosync importer. +- **Telegram:** obtain provider clarification on AI use and consent before general import work. +- **Personal WhatsApp:** evaluate a direct local linked client only as an optional experiment with an explicit support limit. +- **WeChat:** obtain accessible provider documentation and test a supported account route before committing to sync. +- **Teams, Matrix, and Signal:** add after the common account and import model is stable. + +These priorities are judgments based on setup friction, existing code, provider support, and maintenance burden. They are not development-time estimates. Provider verification can take longer than implementation, so start registration work early. + +Before a provider is selected for release, run a small disposable-account test that proves: + +- First sign-in, cancellation, restart, reconnect, and concurrent account behavior. +- The account scope and returned identity match the user's selection. +- Data requests go directly to the source and AI processing stays local. +- Paging, token refresh, rate limits, updates, deletions, and interrupted sync are handled. +- No live-search-only data persists in memory, model history, logs, or sync. +- Sending and calendar changes use the existing action controls and correct account. + +For EventKit, also test denied OS access, signed-app permission attribution, multiple calendars, all-day events, daylight-saving changes, and recurring events. For IMAP, prove that a fetch does not change the Seen flag. For Slack, prove private scope revocation and the chosen retention rule. These are proposed future implementation checks; none was executed against a customer account for this report. + +## Evidence limits and unresolved questions + +This research inspected code and current provider documentation. It did not connect user accounts, install integration programs, run the app, or run end-to-end tests. No credentials or private profile records were used. + +The following items need direct confirmation before a release promise: + +| Item | What remains unresolved | Required next evidence | +| --- | --- | --- | +| Google verification | Exact assessment scope for the complete Off Grid configuration | Verification-team response to the documented local data path | +| Microsoft organizations | Consent behavior under customer tenant policy | A disposable personal account and a managed test tenant | +| Slack | Marketplace eligibility; retention rules for MCP, RTS, and derived output | Provider approval and a scoped internal-app test | +| Apple account authorization | Off Grid eligibility and developer registration for Mail access | Apple developer guidance; do not infer it from consumer help | +| Fastmail calendars | OAuth credential compatibility with CalDAV | Provider documentation or a registered test application | +| Telegram | AI use and all-participant consent implementation | Provider clarification for the specific product behavior | +| WhatsApp | Consumer inbox API availability; business inbound path; current agent and business MCP developer access | Accessible primary documentation and a disposable account | +| WeChat and WeCom | Supported personal path and business archive requirements | Accessible Tencent documentation and provider-approved test setup | + +Primary Meta and Tencent pages were partly inaccessible or required login. Community reports were used to find questions, not to establish release capabilities. Project documentation for Baileys, Wechaty, and signal-cli establishes what those projects claim; it does not establish provider approval. + +The saved result is a research document only. It contains private-repository source references and should remain internal. The main checkout's application files were not changed. New provider code should be implemented and committed in the Pro repository, with separate core changes only where a generic interface needs extension. + + +## Obsidian implementation choice + +User job: connect local Markdown notes from onboarding or Integrations with one folder choice. Reuse the pinned production `Card` and `Button` primitives already used by Google. The native folder dialog owns folder selection; a text path field or third-party MCP process would add setup work and is not selected. A private provider supplies read tools through the existing core MCP tool-source hook. The grant names one vault directory; hidden directories, symbolic links, non-Markdown files, and paths outside that directory are excluded. Notes are read when requested, without a background import. No new tests will be written or run until the user agrees that the product works. + + +Obsidian delivery: onboarding and Integrations now include **Choose vault** and **Change vault**. The private provider supplies `list_notes`, `search_notes`, and `read_note` through the same MCP chat extension as other connections. Folder grants are protected in the device secret store and are removed when the connection is removed. A changed or unavailable vault requires folder selection again. The reader skips hidden paths and symbolic links, accepts Markdown only, limits each note to 2 MB, and reports bounded search results. No writes or automatic memory import are available from this provider. The build and Pro type check passed. No Obsidian tests were written or run. New profiles also leave clipboard capture off until explicitly enabled; saved user choices retain their previous value. + +## Microsoft implementation, 30 September 2026 + +The private provider uses direct Microsoft Graph v1.0 requests and the common +Microsoft identity authority for personal and work/school accounts. One public +application identity requests delegated User.Read, Mail.Read, Calendars.Read, +and Files.Read permissions, plus offline_access for refresh. Authorization uses +PKCE, the existing system-browser loopback, cancel, and encrypted local tokens. +No confidential client secret is requested or bundled. + +Onboarding and Integrations offer registered Off Grid sign-in or a user-supplied +public application ID. Configure MICROSOFT_CLIENT_ID for the registered build. +No Microsoft registration was supplied for this worktree, so registered sign-in +is visibly unavailable. Register personal and organizational account support +and the app's displayed loopback URI under Mobile and desktop applications. +Microsoft requires the 127.0.0.1 HTTP URI to be configured through the app manifest +when the portal field does not allow it. Work tenants may require administrator +consent. This implementation targets the global Microsoft cloud. + +Tools read/search Outlook messages, read message bodies, list calendars and +calendar occurrences, list/search OneDrive files, and read text files up to 2 MB. +Binary Office documents return metadata. Collection tools preserve next-page +links and restrict authenticated pagination to the same Graph collection. +OneDrive download redirects never receive the OAuth token. Connections stay +live-only, without automatic memory import. Changed own-app settings remove +old tokens and require approval again. + +Sources: [Microsoft authorization code flow](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow), +[redirect configuration](https://learn.microsoft.com/en-us/entra/identity-platform/reply-url), +[mail reads](https://learn.microsoft.com/en-us/graph/api/user-list-messages?view=graph-rest-1.0), +[calendar occurrences](https://learn.microsoft.com/en-us/graph/api/calendar-list-calendarview?view=graph-rest-1.0), +[OneDrive download redirects](https://learn.microsoft.com/en-us/graph/api/driveitem-get-content?view=graph-rest-1.0). + +## Design correction and reload recovery + +The sibling brand guide at 15e6b2f (7 September) is newer than the Desktop guide +at 332e48397 (27 August). The Desktop guide now includes the canonical content +and a source record; DESIGN.md refers to it. The connection screen reuses the existing Desktop Item, ItemGroup, Button, and +SidePanel components after product review rejected the earlier card composition. +Shared Input, Label, and NativeSelect controls remain for the advanced setup +form. Row composition wins over tall Cards and catalogue demo controls because +it matches the existing Desktop patterns, keeps actions beside content, and +moves application configuration into a focused side panel. No Card or Sheet +copy remains in the connection screen. The pinned operator dependency is +ca35d2f9004b212ea034b73109fcd2819a08816b. + +Locally bundled monochrome app marks identify Google, Microsoft, Obsidian, +Notion, Jira, Confluence, and Linear; generic service icons were removed. +CSS layer ordering places Tailwind resets before shared components. Previously +the reset layer was registered after components and overrode spacing and text +sizes. New profiles now start in dark mode, consistent with the documented +Desktop default; explicit light and system preferences remain respected. + +Packaged navigation now keeps the renderer HTML path and stores routes in the +fragment, preventing reload from opening missing file:///settings pages. +Onboarding loads the provider slot for entitled builds before full app activation, +so Google, Microsoft, and Obsidian appear with the existing MCP providers. +No tests were added or run during this implementation. Real account authorization +is still unverified. + +Delivery checks: the final Pro type check and production bundle pass. Core type +checking still reports the existing chat-stream video-phase contract mismatch +in src/main/chat-stream-state.ts. No tests were written or run. The isolated +review app shows the connection rows, correct app marks, dark default, and the +Microsoft own-application SidePanel. Real-account sign-in remains unverified. + +### Google setup correction, 30 September 2026 + +Enabled the Google Drive API and Google People API in the Off Grid AI project through Google Cloud Console. The owner approved the People API terms. The Enabled APIs page confirms Gmail, Calendar, Drive, and People. The worktree now offers Add account after a successful Google connection. Each addition creates its own connector ID and protected token keys; Google shows an account chooser. Successful verification labels the connector with its account email, so chat can distinguish account-specific tool sets. Existing Integrations controls manage each connector separately. Setup errors render below the row and disabled-API responses use short actionable copy. Production build and Pro type check pass; no tests were written or run. Real sign-in and multiple-account behavior need owner review. + +Obsidian update: the community Local REST API plugin now includes a built-in MCP server, so a separate wrapper server is unnecessary. This requires the plugin, API key, and a running Obsidian session; the existing folder reader remains the no-plugin option. See https://github.com/coddingtonbear/obsidian-local-rest-api. + +### Microsoft registered application, 30 September 2026 + +Created Off Grid AI Desktop in Microsoft Entra with the owner's approval of Microsoft Platform Policies. Public client ID: 2bbfd68a-f7ce-4327-8cb9-45760b77c27f. Account audience: organisational and personal Microsoft accounts. Mobile/Desktop redirect: http://127.0.0.1:33418/callback. No client secret was created. The ignored worktree configuration supplies MICROSOFT_CLIENT_ID; release builds need the same public ID. The production build passed and the review app was restarted. Microsoft displays a publisher-verification requirement for customer consent to newly registered multitenant applications. No tenant-wide administrator consent was granted. Real account sign-in remains to be reviewed. + +### Per-account service selection + +Google and Microsoft quick setup now opens the shared SidePanel with service checkboxes before sign-in, in both onboarding and Integrations. At least one service is required. Each connector stores its own validated service selection in protected storage. OAuth scopes, Microsoft refresh scopes, service verification, and exposed chat tools follow that selection; tool execution also rejects unselected services. Existing connections without selection metadata keep their previous full-service behavior. Google identity fallback reads skip unselected Gmail/Calendar. The existing shared Input/Label primitives supply checkbox controls. Production build and Pro type check pass. No tests were written or run. The Google checklist is visible in the running review app; actual consent for a reduced selection remains to be reviewed. + +### Account management progress + +Added account lists and per-connection Remove actions for Google, Microsoft, Notion, Jira, Linear, plus Obsidian vault removal. Google/Microsoft Edit access loads saved selections. A new disabled connection holds sign-in and verification; only a matching provider identity can replace the existing account credentials. Failed/cancelled attempts leave the original connection intact. Duplicate Google/Microsoft sign-in targets the existing connection. Older Microsoft labels are filled from Graph /me. Existing duplicate rows are not automatically removed. + +MCP account identity checks use advertised Jira/Linear identity tools and Notion workspace metadata when available. Notion self lookup follows the advertised input schema and requires a workspace ID. Missing identity leaves connections separate; universal deduplication is not yet established. No identity is inferred from a shared server URL. Provider-managed permission changes for those MCP servers still need their consent flows. No tests were run or written. From 9957a229135b670970f8c02d89479b0b065c6a42 Mon Sep 17 00:00:00 2001 From: alichherawalla Date: Thu, 1 Oct 2026 12:48:25 +0530 Subject: [PATCH 4/4] chore: pin provider integration submodule and include review evidence --- docs/research/assets/connection-onboarding.png | Bin 0 -> 181946 bytes docs/research/assets/google-apis-enabled.png | Bin 0 -> 86737 bytes pro | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/research/assets/connection-onboarding.png create mode 100644 docs/research/assets/google-apis-enabled.png diff --git a/docs/research/assets/connection-onboarding.png b/docs/research/assets/connection-onboarding.png new file mode 100644 index 0000000000000000000000000000000000000000..287c84ec1fc79cdf8a5cb762f802c2f39c95f5a1 GIT binary patch literal 181946 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 Q!+--I4j?xNP~qeM3kAM`6#xJL literal 0 HcmV?d00001 diff --git a/docs/research/assets/google-apis-enabled.png b/docs/research/assets/google-apis-enabled.png new file mode 100644 index 0000000000000000000000000000000000000000..06fc2069af83573fe3bc563fb07b5f2d131772d2 GIT binary patch literal 86737 zcmeFY2UL^WwlEqCxVL~Fsv1D|rW4u*32s1es|18l1X2i9IwTYgO@qKzkCY&|6Ql(J z2`wZ9qy!8YQ0cuT^r8qzN046sZ1=hM+&jj7|9k%%_x-)yH=9+7+Ip>;d zWzM;d29G`h{$^rmYzR1Z3;;OB{Q-_f05<`=CwX{zPV(~d@SZxw`@`wKoj(2JkEcb> zoaO)9Ig#_{&xweM0VQQG0mY>y#KeA5{z+O+UJ;~t;gZTPDhj{IDkv)aiR9R+Q>RY< za9a5EX<-GR7*OGVxg5O%2>x(FgXi9fV^V9y_WBNc>6aiDTRX__uO`OYQMf z$9~|pqW%UrcH-EHA9;9>{cwu+xuy+D7)U$z~M`PWXeA=|ED+bMnVVJJj!tKtbvgkcwlCj6`j89 z^l4-aCpFtlrRU{Tn^5pddxFab@ICqVrp*rCiNmNs6{DLL#<)75r{wCbl?Tgv%%Fl7 z4!pG^n&J_=lHH*mPyCMnH><<}n!qD~Pq*(8U^e9X5=o%;;LNNKpc`{gcD?clkTrIy z!b|H2u&$f+`o#6(f1tiN^lIb?(7hb_PT>C#;lp`Bed>Vx!80m41(X_3@|y8&w02h)PXERH|p%R%}7Q{nd zZ#iWE;MI4Y9D6S7tLcZR@^CR{Lux*Y^`v$s8@W)`g|?^pZ}@N102f9ZK(7ey^DPvfT@6p+1)c?*ykpNwA))+r>U`_=7#+6|$+44o8lW!3s7C+y)B zQEmgju?pz?_>8fg=>UVff)M2V)vJ4m&WSngj(J|MYrq*6Wd@{8O(v7a@lmq?uhM)4 zahHaZnqPHF*|cE4BtmSF(L)Y$ioH`%{6u=m4ppEJv*Sc>VY(N;%s4;aqq2(j3OWm+ zX4wciy0582|C7XopTtd5+s=$vD1d}NHR$8OPAlqTZy`~B1AW7D&h7@55N~bjJUY4- zj$kh#wzT!VZq@g5AkX@Q(d14FAjs#>`J7VPxzVO`HNKSiU?iiEwAt539xa9~!JQJU z;qwtX?z~RZvP|7fYqSNwN!6=7Y<**XP|L{nIPSHs?EM~+W>ltldEZm5ai{)Qhq`6h zqKPs&V=p>>b~HAuO@m3>LXXF8XKgvPI8B3@P$i#g_mVpF*h7DhJo9MM#>4b))g|E$ zjV0Bh_bo}_$h+0>UlWo7ll$JCikttFIxG{m=Uws4+ubh7ER)w;9fGJ2;0zSMCGDv) zukmOOjZzQjEuB_D8XC(aBF6C?Y|3ZzbMHxmf;g+0Jo!-4z%!**5f#gW zKwqY$Eu)<>{rB^Ojcv|Yucf^xo2oErL0#wbEYDBUG5K}kYn8Xy%;c^dV_#LLENMU2 zt(}P^mfh~@91gvqcmZy#R3m2Ez^XhrXZvuuW-n<{>3O+^VNFL~e9^!@#r6eaky7lM zR}ilQ#yQ}$PDx2IM6JJkTkH=C?zBf~3B+C3O!TWjqtmndnmkI%phzW4^*b2E3Uz_0 zW|7r6qO}oJ2^F|_I~=Oy%ZED|Ak|eT!uG|H9j8(eQ)sGG@kpg*F}XR`Q%Z47XDGYu zfNt$Uw;I+AOqNd2C-sZi5$^6^dAJO%3AsLDbM1u^sF&1}SDar|f+^e>^=>^TB zr<~YEdriqMnJJsI>UJ^GGw8BAm?0O5$q&>b(PK*JGKJE#pI$Ymtm*fW%X8v^CM0U~ z@Qb^pU_V9R-oZ+gmHel$mir-M6VqZ{W%S`Rx%jpUI-X_R+?Acrw!7vFfXwWgAYc#DgvE0byuBhMYlfHB9w~W4uxB7~&uhSw;;s)C$Fb8D=3u<&=a zTi%?+BqASeWVZU5!rd>LSvR@qE2!A%P(iyA5xl{=%jg49QS09g3AG9BS@?9&r~OwW z3w~14CCxKcaSq>18wMPB-f2n7dZ|wp?i5%u{D;x)0Wn?-(|&;=-+tiK z$_vwfZV=INBSin}zCQESItGCSJsdCiNjD(H)4H2kAy1-K#ji}ujs$h=!N z%w|Gs49=07OqFlltGPZpIPn5^dI1stxTd9I(OW55Kq4t!6|EEH5H7|P^$Y3o+Ytxy zSClE0>enj%9ABWIKnx-jxmrOwobXdME5xzF&va^<)7m3%w=sSc7-O@(8I~=Oje>hM z4)&zd22LVDpOK)kh_>pAB_7QOH{HT5*0QBh#C|$FOKH9rNEsXh-pXO!%jSgo!I>01 zwNl=9>r{kSxRp=U1LZ*lpFLZ4QBk&gsJDW5ud&plIh^+4np9Y^Lqs5Wf>@ZFrl^sx z;%aMdiAHEN=ozcl$$Hb+a`(%HsG9L%zPs^*o3JpK*%XnA5WTIr%^y)F9yPjxa zd{tdqG~R`3&sl=6T&X4Ebe&Vpk}A>}Zcz6$8_92GCIvXBkPg_{+}}_0 zR=qb34n-qBQ?Z@XM}XK*A4e6p#f2U2sEW(|fSk;SJZc#&w7<4lyQ(JRY5LqIgP=_( z*uJr!VhM(u6nxVcC#<>~8jmC;GF~hfxF=hP#9GZc#gs7-GdbR212j&hTFB_$i{_1( zIbqU}DD7z{wCCIuHS_Xh>M0`ATGiV2Z_~OUNq5*yWjN}~q}#yeoq>XT_EUBYDKR0Z z-J4KhLiK*(Xu^$3I_sB zER!7zuvzJzLpzo(sGh-1TNGIH_La*}l(l9Y@qB`{<*KykMLLwY9_nB1&UUCG zSis;Go>a-39uH#!rXNz0nSw$~e)Y7W;lLg+R1Qm|jv;+y|)k%MCXeTDiq&TSS~*j2;h<=*#s;Ta8I-5iR=)RC&8`P?1d<-sDKo9L9EMl#)k;f z?l5Es+T&+R!p3kLj#b6`elW`q6;C0)=4cYHHS?z+A-k zbTp?HfV`9hD*{D-M9YSmY8H0XiHlpvn7Mhy&@COgtHvob8Ihym`!cz>fI$~GB-+AK zJ#(nJZq!B9K!vFQHzrsrJ82rayi^TuV8|7=odt>exSRQ=TZn(o&&$SpYTduY-%jWv zh)N7N(D-Z%XkUi&v=Huav*&(2)>d`=FD$_c!&v?O+YK@?BIuFiN+D)j*x{m|hO&~@ zpgpyyf*yJMx1rg|-1rO<(xmX+j>Manbl{Rlr*(<7^^)t7V z_kmg^vB^b@{5+CZVa4tC?U&Oi8O%x(UDztGwyJn^-n4_M4DP3T?zCcWI#kq@7=9wZ4{-6m%Rj|`;M9gd9asUAT=Wzu9=<7J#gC7Ba5rq6tdsp_B zko(?;hMA8o3tK^Rg$p2CQZTp+lfnrYF7ojzGC(qgZL=-mh~>{0@qK|}fr*GjcS$pS znOmOOm}K4X9Md0)wu;%wpow)Yu{5PiCN-TzEf}MDsxx;9C$)gy?_KBMf+gB_z;K&H z_*pzL$y+Yc!>UfH3_arlLFx(8LJ0$oF*LqV1AzsV)+>Eb4exXH>v3NT&vr7}22)gP zox%o$!!3u}^W3_K9-mXad%=%(>IuN2cZF4Z%VxHW@${M8ejaph>+Iaz9l|%mFMSn7 z@Ge6(_Rd3U@@Cb6Sv9;Z>`vi0YBAGp{+x!t9hSz z%URwwSoev&oh*$o@>;1oBLguY_%Ug8HOS)5sH>H`GSJK({3fLYp_6*C*|E^C-0*X7 zIRx%_1R!1Pjpt-h3w>V>EB-`HyY@1D*~owz9dD|Tv5V%=b^WfIxxn;W7>c&rRwh&i z#bvxw`W_=_eu!!oQ;nsUKP7XiIe8{UIT{WuzRYIy=E(o;{%54h^iH;WBd`|#XDLV zi1m9Cx{HRM&~0>tf^{En+9_(y$=M%A%aA16-V* zm<2~`e?#)+K$H14X{$qdPWojs#Hrj8DyRWpJuuQ<$9P2NVxKGd(h#ezLKl^%Vf&wMmRkXdZai%a<$f~(i zJ(`;Ga-*# zaJ|k9W8N69&&T8v#wWE`KYS^n?!BJG%C>~fH3%e(_N#Y=68_w#X;>)wWv-qHcUdILn2p0|`eJ_a`r*>&_w=if|Ea4>Uhe4p8MW0Vg!)4$1>2r_d! z&-dcBh1XEv`mGIj`yzCe*TwpW-r{31cuI8DFn8-wE(4Lk5YMBVf>zD=kw7A!)76LjQdED&TWSd^*gXau#J4(^t=>zd{L7`xxfVT(4sA=;gR&kSH_ zUO~*IMC7~NGsQ$1L_nfu9_oj_GisFjW1;ksKaxz;z5;19L*Re77!D<~{y08LUvRq7J0f-9rdu-PO7xZJP4PeJcb9x(!3W|Nh9OEQ9sd%J86OAI{jnz3cY%C;;YUQ-Esie4H9dK0f0%f!DUGM!_3 z!Ot9L3MQ9dtFsr@yC7G?WMlT+^oUeFeuB0N?b^kjHSCw{eosy=>-Z z3t7PX?vE7`IcEbL#8c`$lnCd9KvF6Ld9@iVGhRRQ5rOi>d82l^NmDNbu5_Mb$hZ_X z;Ix6MiI841%`Qvh7gGP$KIH6sL*^HKh!1A)iz3f;cpCuStf6n>geG)6) z^j30SHZS`05dh<@Vq$Cu7P2-iW(gKUu-OFCDi}WCfJ5)3X!-IKduToQ>2dnAcg>%( z-A##&#f>Q$DTH67z{oM^yIGafvlLg?T%RsvH6M!KoX}e7G{|-$Dn{;;%hDt-6OA3` zpLvV;JY5b=#7ED3GGK{{29Xa!pIR?ix?`f0We45qi2UD(aAmt4(x~eB=Vgwg);y53$`&FUIu$h>58RJ(n z(MX*pQ7Ya0#o6v4xXm_cYCq>40m#S)jhKD=V~AyDs{&USgH=t2$y+T zGq(;=NaU7|vZiKNBs4LI%7{)03J#@>I``UaK25s}DiBI~K;Q^Gw7N9|M#;2E!zG{k z!D?+Cuv_y${vB+5M?AgfTE2!;MAwx4(~- z^N1|y@mepls>=23YOz$6LDvE+wKC?Zpn1FHMY#Fqlh**j-@YH({%4hhz!NvCX?KTQ zv|UFK!jEd=r8qimc%zZ}FkvRoEY>ttFNx;b&06q)_tK$v0uqO!vWx5zgGTesw*XMHR$jZq(AZ< z=QgUhZ<>c@zh(HEQz}*>h!j#1Y_6Q&&0_r2Sz0@qb_5VV0(9n~V%#H`^7~xZh)$|| z`a6KgU#jzKM5~zN?Wv`c>+1=np$X-F8 z+rB7|Lh$o#t)xlRs-UTO))ByUu`txlMLXUI9Q3DG`Qf=W_>O+$#FOFvPQ7Cn>3Fds z(Nt>&{Na^(4jq23bSdIqna{A2M*3^i`fF>0^thO4_Th1weWVQlzvDmkh3k!~l`5=i zbHNjBhm)ZX6LiN3R!I49M%CxS{koQjMO$+|_Q%5L3+O|m#Wq!uvI*E$%L?3_FguYK zQIn~=?0}3|WW~jK+iHrkD}u}%(va?TKy}U#8tCiaEd+7&xGESK3B8C_(Dq0-!r}Tb@h&014ECxaO3o_p*7-m z`yZ`?)18e}x{=&D`%KyXWHfjpk?)#Giw>zdtaU_de)xekBi@5C8D0CXv$;*Gw7yI% zzJ%>(5G)g>>ljaHTh)hPi{wH3?f$L4p-8e3^5dm8a21p(-v-6-kJGvp({XzMS9CXlj4^yw=q3l*g|bR*{EjeM|_>F(87-ER0!maJpm3wzB#%9`Cjg)4E68s5$Ts zYS{)(>Hh7Q=zji=6Jv@|y4#0jB0xs7C6ynZvEMqxLPuzI_HJ^{LU>{Grv!z8FNX{%=!p0>uf40~)$8CM7>nfz;&O zq$C|MN*j+(x&Km60LBKthFh^*Gfq|98qJiK8pnsBd@;aI26fR+tNYO=L?8wwa+%g6 zes!%1QpUr9%5j4|6oH27n4Q~x&+n5fzso0&FNQH zVIPLPr+f(2B^`F%Sx@y+p~^*x=V$XmeEwj$Vh_GjowhUdQjY-XMKec$MF-w@+&HyI z#1{;+cJJ&zo_t342TwRHcI_{PTsZ7^BsYRvTNTMqZ0>F z|9C#le(Q6rc+>MRxSDH3U;q0{c}U!&(^YT0rJHWAqd}wWC8Qcw9$nnS@9!bsS;4g4 zcPXz318b%Zeny%W1P2=_P0V1aaVIIxNw!f4PT3Xdw8sj`wvC;f+PyZc)26|WIN-<2 zcL6+q@d>?jz90B(>rf&(dipC<_wWd?DdQ(9*i50|V~6oBI-QS@qmA{;m_*^<$Ik!O zz8Pp|{Py!ukFVUfSpHu;LUmMf#_ui8RwO)Vh&+E;FI}wH|JUy@lXd(TGCE_uue|&V z#yOI|%M((b*7+CA>pEV~GV-w5my6$FY@DB5{*LtdcNpbg-lu*?+VmYp?EIa74gCKB zR%hb5Cv-ah#DCO-cX%y`xNA(Dn=h2qpW7TW8Xfw-q()a+t;yg5yb&5>J(CymU0u{H z%tX!=`Y_vur|mcqqGFHspkglRgb5r2T#Va40>p5`)5m}RhnY|>99HnzRhaWm>4!*e z+p?KL8(gm>lgMqBm>N2paM@(%l1tBL+@xJ9ck$e8nz;x)0=V?vb<8*A=>dXl6ue#X zvY-CZ*O-D57n(wg0hC_0(oU00$w{0N4_|O4B;0#>Th?W>@d&V7{8id$=f88SlnbVnKeYF5RoiZ!B0?&Cx_TPs)@#cyYQ~&g~uZ3Cf&9E5I3gRCKnRsi8q*Nz5RZ@1flJ-px$B`sBH?!UeEW zssP=TbIWlh(s1)*Nbh08j@sydV-M~8rwDV1{fs?s+gi?H3SgO~S73NtRH zUPIF77+mj}g{S9I;+;lRY+YO2kazvdo}5~|w|71*{dI4h%r|X6pL-1Z={>1S*nPsG zsb|^3&lPL&GV%8h=FyP;!jP7g-TW4P2O9nC>pm;%UUDw@>TUAo%wSyYBiH+5HH9YF zJ~2LvlRww+lIC|K7mst=!bE%i?t&n{_)zf~tQzvvNHp1a|@8LHzIUVC9`EI#|pK zzXdgW{^9Tkn-0b7t$$Fslh^bq;v~=~{M^?Yho6m>x%6BcRlJ`lwTz>o|n$%pM|9vw1e_kv8HNyYp#pWQMHcYCCdZPKP zXTE--z66Og{RyD;JJ%|laU86q&4HE#fFPt*_e>eH-DWI)C34B>TFT}|MENy1^wr!h z&-iA*x!9`D$QQwKsSbV!75-9o;~%n0Jw=Xe^fiI8sstzB#@_d@y&cfDVsY+5xoAWS zDRG;)=@beVF0tBxzkOT*&h~+gB))(XEVV#j7nALtfm(5OIlh6#Se$(OY^O^dEpf-l z%VbZ#=KR9t`MSY*VtxdrgM3@_SaqI`>(BXcv$f_k|Ho^Rb z?N*Zeo3>a#+PC0(YRmk=1z(-rTNkVdT2OLob8@ylJ{^yDZE*4%ykL3L-L|4In5CC% zW+`Juzx;F3sru+8uZf@znB($DJgQ$HZoAC#!H*PAwz)0!IVn&9Z z7Qx`?y*J@T-9GJvQXeE7-u?b6qxVLM3dGDHQRvgshxpJ$OSK)@#yoaIoO^!DVr8Vn zvzFFs-wkjuJ=cVoqxqsJ2?ZBa*S-Km8?eChW}YbLVRrCXnq*MAbWT2w7B-_KF(5_N zE<%$*++v^K=B;3NlXwAGLFc`F8*Y7DIlQ~f71v=hUH1@c5C`Xmp3>lI28(Z3uE}$P zSsoUJ6$|cNGHY5t%0E$8?k4X&e;C2R=C@Am301iaJeKXM=&4xpZxt=^yghxA`)5Tz zeRXKzgp9Aj84SWmZ$fm7nwJY@jqi1lwc$W&42~Qek|`8)b<>Pk9^*FLGo8y5xS`1? zU$;oo8PJE=jY)-4n@Vg4q!U${y3cc`^vsG;#JlVfl~CUYVn}0=RS%-PYfw8=*v{HY znlT;|ws5~FUF{?^p?Sne0UMveXO*T{fMCjT(_vpv<-4NH+e37~Fw+Icn89f_ZSBp1 zN?~cK4}Qlh-?kMWZ9{}<5sAj@!K&qf_{Jhh+l_^4)H`>X;#S}-tE(yH3L3kL=LhGr zasw{#^|XZcXXy<vlzNbK(3IV%F1qbZsnEfi}Nhh4Q&SrW_!h!hT2T*S1|p zP(uf#avSqf(tDR@IBlW4?U7wXbdyQa-_1#Jij#q~+c8nrf|SbQnlY#F>C21CuU=J5 zQSfmdKI=;{eK)U~$E@bW)sOVFe6yIuXG%x4j2v4RTfRBtGrXf1sz`(dE`IQybGyyiUH^7fUowb{ zuPKJ+CN;(NHfd^I2GseP~i^cFE$y^iI@}g?r&`(~^OnqHr8MbhS`} z5r@^L(!wjKQe-*uRQ`>%UEdgV&q%Njd^3s&F>ELlDjv@9H60j%FfevtlyOw zfK}P8=KEo+w1=UnfjD3sw``Ue}C9{?t#6qF;9#fa_N%{>@Z~3mQ_rT*dW9v$vzB&#+%F?Z2t;X~W`@tWZf9%3QjDL|-=gi$Oq6 zjh4=|tCi-!H|(R0x7n48?P+DbRYL`~IL}*ygck8BcxbhtAca=E=sb0Z8oY2_)*#r7 zJmit@fwjpLpCr&%@8F~p^(j%EDlc;>T#rULWO&t@Zk9KKeoE})<{ZpNPk)#J71xEP zeOrXwB>SfM+cB-7{v*L~y;z=PBTIfutz3o>TL(c_l?Mw7#?|5E7_8rx3u8@)gJp_z zaV@uu_&m)dMM3JQnPUUfQuLaz^7hl`mcH+It~Q&A*?DBVv(wsbS+uzThRQJCs9{Lc z9b@=-#;wH@`rP&Mm$gc<$1-(Y*Wg1oVpoKdzr3?WUR(%}Qaw4R{-z1sqk z*QU?OQdNRcJ+{$h`tJE|%v7OMZ9|XBl$JwiCI#Mwf^qTP@b7*30!#vWjzZJ7|v2nz21p2ys}=k`$0k4C0RDktQOZau@i z+_B%J?~W#`t8%tZEdI80+)mrshwY}92DaZ6aL?w!*{b%Ch@cxK*>0|mu}?c8EPdm+ zAeH98+rx|pN{MZrb`?3CU-V8Ma_p{M*w335&2xQqe(}ZHx#VIJhI1&uDQ%?iY|#m0LQqFJ*5z?QQhD$PJv=Gk=MGOlIFysg z!8%~`=w4L`ugCs8R~>ug70#^vdIT6VwFY$`1Vnr~0#v2sc; zM$x71Sz$(|%(smIFB0TRIoa=dph?_TqH3_r0PXeqr;lcV9k#v`l3%~TA9(z{F)0dv z!A=RJMmslzOMLFB92*Im^06hgR%={X(mCtNWtWc6H~04eiNfQ4xhz$u`p;{JB$K!F zmguZ9ito8Xcx(#YF5AHpv+Y3FZ&ax3a6~OkG2mWrbyMWUMuXK=%oE{?>cNvI<0iGj(ror2`d#z4$)8?ucN(;B7PR&6Dh zc;Ke+gdB#YRsGz>@>OnlZDxV#@JMYh^`j|L#AH=kW$Y#AZ#T~bduPY1IPQ%`kA;!C z8aaJcnfk-gNoU$8^?un3-n^thsxp~qVX5%5yE3Sq<#V&?X0XwbT#UP)@5OjCn-qG@ z`ih21wHURl) ziKarcN>-8>Zq3avEvL*)+RUYCsu@Xzmg-5yYm2sF9h2pX`rMPW5*@&hOo)fY?bg

ji+ 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 XaqT7~zKDOj0{$I7O5!gTZ+ia|Z81fe literal 0 HcmV?d00001 diff --git a/pro b/pro index 08531ba65..f358ec284 160000 --- a/pro +++ b/pro @@ -1 +1 @@ -Subproject commit 08531ba6521de1b6b3e257ece01269cde88bc585 +Subproject commit f358ec284b2b03f3d8218b78072b4761f25fc720