diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index f3bfed02..9107189a 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -87,6 +87,8 @@ import { toast } from "sonner" import { useVoiceMode } from '@/hooks/useVoiceMode' import { useVoiceTTS } from '@/hooks/useVoiceTTS' import { useMeetingTranscription, type MeetingTranscriptionState, type CalendarEventMeta } from '@/hooks/useMeetingTranscription' +import { useAnalyticsIdentity } from '@/hooks/useAnalyticsIdentity' +import * as analytics from '@/lib/analytics' type DirEntry = z.infer type RunEventType = z.infer @@ -624,6 +626,8 @@ function App() { type ShortcutPane = 'left' | 'right' type MarkdownHistoryHandlers = { undo: () => boolean; redo: () => boolean } + useAnalyticsIdentity() + // File browser state (for Knowledge section) const [selectedPath, setSelectedPath] = useState(null) const [fileContent, setFileContent] = useState('') @@ -2163,6 +2167,7 @@ function App() { currentRunId = run.id newRunCreatedAt = run.createdAt setRunId(currentRunId) + analytics.chatSessionCreated(currentRunId) // Update active chat tab's runId to the new run setChatTabs((prev) => prev.map((tab) => ( tab.id === submitTabId @@ -2223,6 +2228,11 @@ function App() { voiceOutput: ttsEnabledRef.current ? ttsModeRef.current : undefined, searchEnabled: searchEnabled || undefined, }) + analytics.chatMessageSent({ + voiceInput: pendingVoiceInputRef.current || undefined, + voiceOutput: ttsEnabledRef.current ? ttsModeRef.current : undefined, + searchEnabled: searchEnabled || undefined, + }) } else { // Legacy path: plain string with optional XML-formatted @mentions. let formattedMessage = userMessage @@ -2254,6 +2264,11 @@ function App() { voiceOutput: ttsEnabledRef.current ? ttsModeRef.current : undefined, searchEnabled: searchEnabled || undefined, }) + analytics.chatMessageSent({ + voiceInput: pendingVoiceInputRef.current || undefined, + voiceOutput: ttsEnabledRef.current ? ttsModeRef.current : undefined, + searchEnabled: searchEnabled || undefined, + }) titleSource = formattedMessage } @@ -4267,6 +4282,7 @@ function App() { const title = getBaseName(tab.path) try { await window.ipc.invoke('export:note', { markdown, format, title }) + analytics.noteExported(format) } catch (err) { console.error('Export failed:', err) } diff --git a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx index 03ab3f94..37d8d053 100644 --- a/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx +++ b/apps/x/apps/renderer/src/components/chat-input-with-mentions.tsx @@ -563,7 +563,7 @@ function ChatInputInner({ className="flex h-7 shrink-0 items-center gap-1 rounded-full px-2 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground" > - {configuredModels.find((m) => `${m.flavor}/${m.model}` === activeModelKey)?.model || 'Model'} + {configuredModels.find((m) => `${m.flavor}/${m.model}` === activeModelKey)?.model || configuredModels[0]?.model || 'Model'} diff --git a/apps/x/apps/renderer/src/components/connectors-popover.tsx b/apps/x/apps/renderer/src/components/connectors-popover.tsx index e28f662e..92b13a48 100644 --- a/apps/x/apps/renderer/src/components/connectors-popover.tsx +++ b/apps/x/apps/renderer/src/components/connectors-popover.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { useState } from "react" -import { AlertTriangle, Loader2, Mic, Mail, Calendar, MessageSquare, User } from "lucide-react" +import { AlertTriangle, Loader2, Mic, Mail, Calendar, User } from "lucide-react" import { Popover, @@ -15,7 +15,6 @@ import { TooltipTrigger, } from "@/components/ui/tooltip" import { Button } from "@/components/ui/button" -import { Switch } from "@/components/ui/switch" import { Separator } from "@/components/ui/separator" import { GoogleClientIdModal } from "@/components/google-client-id-modal" import { ComposioApiKeyModal } from "@/components/composio-api-key-modal" @@ -126,8 +125,6 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha // Check if Gmail is unconnected (for filtering in unconnected mode) const isGmailUnconnected = c.useComposioForGoogle ? !c.gmailConnected && !c.gmailLoading : true const isGoogleCalendarUnconnected = c.useComposioForGoogleCalendar ? !c.googleCalendarConnected && !c.googleCalendarLoading : true - const isGranolaUnconnected = !c.granolaEnabled && !c.granolaLoading - const isSlackUnconnected = !c.slackEnabled && !c.slackLoading // For unconnected mode, check if there's anything to show const hasUnconnectedEmailCalendar = (() => { @@ -143,7 +140,6 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha const hasUnconnectedMeetingNotes = (() => { if (!isUnconnectedMode) return true - if (isGranolaUnconnected) return true if (c.providers.includes('fireflies-ai')) { const firefliesState = c.providerStates['fireflies-ai'] if (!firefliesState?.isConnected || c.providerStatus['fireflies-ai']?.error) return true @@ -151,15 +147,13 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha return false })() - const hasUnconnectedSlack = !isUnconnectedMode || isSlackUnconnected - const isRowboatUnconnected = (() => { if (!c.providers.includes('rowboat')) return false const rowboatState = c.providerStates['rowboat'] return !rowboatState?.isConnected || rowboatState?.isLoading })() - const allConnected = isUnconnectedMode && !isRowboatUnconnected && !hasUnconnectedEmailCalendar && !hasUnconnectedMeetingNotes && !hasUnconnectedSlack + const allConnected = isUnconnectedMode && !isRowboatUnconnected && !hasUnconnectedEmailCalendar && !hasUnconnectedMeetingNotes return ( <> @@ -357,128 +351,12 @@ export function ConnectorsPopover({ children, tooltip, open: openProp, onOpenCha Meeting Notes - {/* Granola - show in unconnected mode only if not enabled */} - {(!isUnconnectedMode || isGranolaUnconnected) && ( -
-
-
- -
-
- Granola - - Local meeting notes - -
-
-
- {c.granolaLoading && ( - - )} - -
-
- )} - {/* Fireflies */} {c.providers.includes('fireflies-ai') && renderOAuthProvider('fireflies-ai', 'Fireflies', , 'AI meeting transcripts')} )} - - {/* Team Communication Section */} - {hasUnconnectedSlack && ( - <> -
- Team Communication -
- -
-
-
-
- -
-
- Slack - {c.slackEnabled && c.slackWorkspaces.length > 0 ? ( - - {c.slackWorkspaces.map(w => w.name).join(', ')} - - ) : ( - - Send messages and view channels - - )} -
-
-
- {(c.slackLoading || c.slackDiscovering) && ( - - )} - {c.slackEnabled ? ( - c.handleSlackDisable()} - disabled={c.slackLoading} - /> - ) : ( - - )} -
-
- {c.slackPickerOpen && ( -
- {c.slackDiscoverError ? ( -

{c.slackDiscoverError}

- ) : ( - <> - {c.slackAvailableWorkspaces.map(w => ( - - ))} - - - )} -
- )} -
- - )} )} diff --git a/apps/x/apps/renderer/src/components/graph-view.tsx b/apps/x/apps/renderer/src/components/graph-view.tsx index 0a847f54..b0b57594 100644 --- a/apps/x/apps/renderer/src/components/graph-view.tsx +++ b/apps/x/apps/renderer/src/components/graph-view.tsx @@ -48,7 +48,7 @@ const FLOAT_VARIANCE = 2 const FLOAT_SPEED_BASE = 0.0006 const FLOAT_SPEED_VARIANCE = 0.00025 -export function GraphView({ nodes, edges, isLoading, error, onSelectNode }: GraphViewProps) { +export function GraphView({ nodes, edges, error, onSelectNode }: GraphViewProps) { const containerRef = useRef(null) const positionsRef = useRef>(new Map()) const motionSeedsRef = useRef>(new Map()) diff --git a/apps/x/apps/renderer/src/components/onboarding/steps/completion-step.tsx b/apps/x/apps/renderer/src/components/onboarding/steps/completion-step.tsx index c01e42ea..2b32309a 100644 --- a/apps/x/apps/renderer/src/components/onboarding/steps/completion-step.tsx +++ b/apps/x/apps/renderer/src/components/onboarding/steps/completion-step.tsx @@ -8,8 +8,8 @@ interface CompletionStepProps { } export function CompletionStep({ state }: CompletionStepProps) { - const { connectedProviders, granolaEnabled, slackEnabled, gmailConnected, googleCalendarConnected, handleComplete } = state - const hasConnections = connectedProviders.length > 0 || granolaEnabled || slackEnabled || gmailConnected || googleCalendarConnected + const { connectedProviders, gmailConnected, googleCalendarConnected, handleComplete } = state + const hasConnections = connectedProviders.length > 0 || gmailConnected || googleCalendarConnected return (
@@ -109,28 +109,6 @@ export function CompletionStep({ state }: CompletionStepProps) { Fireflies (Meeting transcripts) )} - {granolaEnabled && ( - - - Granola (Local meeting notes) - - )} - {slackEnabled && ( - - - Slack (Team communication) - - )}
)} diff --git a/apps/x/apps/renderer/src/components/onboarding/steps/connect-accounts-step.tsx b/apps/x/apps/renderer/src/components/onboarding/steps/connect-accounts-step.tsx index b152d567..ea8335de 100644 --- a/apps/x/apps/renderer/src/components/onboarding/steps/connect-accounts-step.tsx +++ b/apps/x/apps/renderer/src/components/onboarding/steps/connect-accounts-step.tsx @@ -1,9 +1,8 @@ -import { Loader2, CheckCircle2, ArrowLeft, Calendar } from "lucide-react" +import { Loader2, CheckCircle2, ArrowLeft, Calendar, FileText } from "lucide-react" import { motion } from "motion/react" import { Button } from "@/components/ui/button" -import { Switch } from "@/components/ui/switch" import { cn } from "@/lib/utils" -import { GmailIcon, SlackIcon, FirefliesIcon, GranolaIcon } from "../provider-icons" +import { GmailIcon, FirefliesIcon } from "../provider-icons" import type { OnboardingState, ProviderState } from "../use-onboarding-state" interface ConnectAccountsStepProps { @@ -85,11 +84,6 @@ function ProviderCard({ export function ConnectAccountsStep({ state }: ConnectAccountsStepProps) { const { providers, providersLoading, providerStates, handleConnect, - granolaEnabled, granolaLoading, handleGranolaToggle, - slackEnabled, slackLoading, slackWorkspaces, slackAvailableWorkspaces, - slackSelectedUrls, setSlackSelectedUrls, slackPickerOpen, - slackDiscovering, slackDiscoverError, - handleSlackEnable, handleSlackSaveWorkspaces, handleSlackDisable, useComposioForGoogle, gmailConnected, gmailLoading, gmailConnecting, handleConnectGmail, useComposioForGoogleCalendar, googleCalendarConnected, googleCalendarLoading, googleCalendarConnecting, handleConnectGoogleCalendar, handleNext, handleBack, @@ -104,7 +98,7 @@ export function ConnectAccountsStep({ state }: ConnectAccountsStepProps) { Connect Your Accounts

- Connect your accounts to give Rowboat context about your work. You can always add more later. + Rowboat gets smarter the more it knows about your work. Connect your accounts to get started. You can find more tools in Settings.

{providersLoading ? ( @@ -122,7 +116,7 @@ export function ConnectAccountsStep({ state }: ConnectAccountsStepProps) { {useComposioForGoogle ? ( } iconBg="bg-red-500/10" iconColor="text-red-500" @@ -145,7 +139,7 @@ export function ConnectAccountsStep({ state }: ConnectAccountsStepProps) { {useComposioForGoogleCalendar && ( } iconBg="bg-blue-500/10" iconColor="text-blue-500" @@ -162,29 +156,31 @@ export function ConnectAccountsStep({ state }: ConnectAccountsStepProps) { Meeting Notes - } - iconBg="bg-purple-500/10" - iconColor="text-purple-500" - providerState={{ isConnected: granolaEnabled, isLoading: false, isConnecting: false }} - rightSlot={ -
- {granolaLoading && } - + +
+
+
- } - index={cardIndex++} - /> +
+
Rowboat Meeting Notes
+
Built in. Ready to use.
+
+
+
+
+ +
+
+
{providers.includes('fireflies-ai') && ( } iconBg="bg-amber-500/10" iconColor="text-amber-500" @@ -194,83 +190,6 @@ export function ConnectAccountsStep({ state }: ConnectAccountsStepProps) { /> )}
- - {/* Team Communication */} -
- - Team Communication - -
- 0 - ? slackWorkspaces.map(w => w.name).join(', ') - : "Enable Rowboat to understand your team conversations and provide relevant context" - } - icon={} - iconBg="bg-emerald-500/10" - iconColor="text-emerald-500" - providerState={{ isConnected: slackEnabled, isLoading: false, isConnecting: false }} - rightSlot={ -
- {(slackLoading || slackDiscovering) && } - {slackEnabled ? ( - handleSlackDisable()} - disabled={slackLoading} - /> - ) : ( - - )} -
- } - index={cardIndex++} - /> - {slackPickerOpen && ( -
- {slackDiscoverError ? ( -

{slackDiscoverError}

- ) : ( - <> - {slackAvailableWorkspaces.map(w => ( - - ))} - - - )} -
- )} -
-
)} diff --git a/apps/x/apps/renderer/src/components/onboarding/steps/llm-setup-step.tsx b/apps/x/apps/renderer/src/components/onboarding/steps/llm-setup-step.tsx index 534a67a8..a9956245 100644 --- a/apps/x/apps/renderer/src/components/onboarding/steps/llm-setup-step.tsx +++ b/apps/x/apps/renderer/src/components/onboarding/steps/llm-setup-step.tsx @@ -104,7 +104,7 @@ export function LlmSetupStep({ state }: LlmSetupStepProps) {

- Tip: Sign in with Rowboat for instant access to all models — no API keys needed. + Tip: Sign in with Rowboat for instant access to leading models. No API keys needed.

- )} -
- - {c.slackPickerOpen && ( -
- {c.slackDiscoverError ? ( -

{c.slackDiscoverError}

- ) : ( - <> - {c.slackAvailableWorkspaces.map(w => ( - - ))} - - - )} -
- )} - + {/* Fireflies */} + {renderOAuthProvider('fireflies-ai', 'Fireflies', , 'AI meeting transcripts')} + + )} ) diff --git a/apps/x/apps/renderer/src/components/tab-bar.tsx b/apps/x/apps/renderer/src/components/tab-bar.tsx index b4374ffb..a81c4ece 100644 --- a/apps/x/apps/renderer/src/components/tab-bar.tsx +++ b/apps/x/apps/renderer/src/components/tab-bar.tsx @@ -29,7 +29,6 @@ export function TabBar({ activeTabId, getTabTitle, getTabId, - isProcessing, onSwitchTab, onCloseTab, layout = 'fill', @@ -47,7 +46,6 @@ export function TabBar({ {tabs.map((tab, index) => { const tabId = getTabId(tab) const isActive = tabId === activeTabId - const processing = isProcessing?.(tab) ?? false const title = getTabTitle(tab) return ( diff --git a/apps/x/apps/renderer/src/hooks/useAnalyticsIdentity.ts b/apps/x/apps/renderer/src/hooks/useAnalyticsIdentity.ts new file mode 100644 index 00000000..20fd6281 --- /dev/null +++ b/apps/x/apps/renderer/src/hooks/useAnalyticsIdentity.ts @@ -0,0 +1,74 @@ +import { useEffect } from 'react' +import posthog from 'posthog-js' + +/** + * Identifies the user in PostHog when signed into Rowboat, + * and sets user properties for connected OAuth providers. + * Call once at the App level. + */ +export function useAnalyticsIdentity() { + // On mount: check current OAuth state and identify if signed in + useEffect(() => { + async function init() { + try { + const result = await window.ipc.invoke('oauth:getState', null) + const config = result.config || {} + + // Identify if Rowboat account is connected + const rowboat = config.rowboat + if (rowboat?.connected && rowboat?.userId) { + posthog.identify(rowboat.userId) + } + + // Set provider connection flags + const providers = ['gmail', 'calendar', 'slack', 'rowboat'] + const props: Record = { signed_in: !!rowboat?.connected } + for (const p of providers) { + props[`${p}_connected`] = !!config[p]?.connected + } + posthog.people.set(props) + + // Count notes for total_notes property + try { + const entries = await window.ipc.invoke('workspace:readdir', { path: '/' }) + let totalNotes = 0 + if (entries) { + for (const entry of entries) { + if (entry.kind === 'dir') { + try { + const sub = await window.ipc.invoke('workspace:readdir', { path: `/${entry.name}` }) + totalNotes += sub?.length ?? 0 + } catch { + // skip inaccessible dirs + } + } + } + } + posthog.people.set({ total_notes: totalNotes }) + } catch { + // workspace may not be available + } + } catch { + // oauth state unavailable + } + } + init() + }, []) + + // Listen for OAuth connect/disconnect events to update identity + useEffect(() => { + const cleanup = window.ipc.on('oauth:didConnect', (event) => { + if (!event.success) return + + // If Rowboat provider connected, identify user + if (event.provider === 'rowboat' && event.userId) { + posthog.identify(event.userId) + posthog.people.set({ signed_in: true }) + } + + posthog.people.set({ [`${event.provider}_connected`]: true }) + }) + + return cleanup + }, []) +} diff --git a/apps/x/apps/renderer/src/hooks/useOAuth.ts b/apps/x/apps/renderer/src/hooks/useOAuth.ts index de86ce4c..6f315ec0 100644 --- a/apps/x/apps/renderer/src/hooks/useOAuth.ts +++ b/apps/x/apps/renderer/src/hooks/useOAuth.ts @@ -1,5 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { toast } from '@/lib/toast'; +import posthog from 'posthog-js'; +import * as analytics from '@/lib/analytics'; /** * Hook for managing OAuth connection state for a specific provider @@ -40,6 +42,8 @@ export function useOAuth(provider: string) { setIsLoading(false); if (event.success) { + analytics.oauthConnected(provider); + posthog.people.set({ [`${provider}_connected`]: true }); toast(`Successfully connected to ${provider}`, 'success'); // Refresh connection status to ensure consistency checkConnection(); @@ -75,6 +79,8 @@ export function useOAuth(provider: string) { setIsLoading(true); const result = await window.ipc.invoke('oauth:disconnect', { provider }); if (result.success) { + analytics.oauthDisconnected(provider); + posthog.people.set({ [`${provider}_connected`]: false }); toast(`Disconnected from ${provider}`, 'success'); setIsConnected(false); } else { diff --git a/apps/x/apps/renderer/src/hooks/useVoiceMode.ts b/apps/x/apps/renderer/src/hooks/useVoiceMode.ts index ce4f7ccb..b5e1187d 100644 --- a/apps/x/apps/renderer/src/hooks/useVoiceMode.ts +++ b/apps/x/apps/renderer/src/hooks/useVoiceMode.ts @@ -1,6 +1,8 @@ import { useCallback, useRef, useState } from 'react'; import { buildDeepgramListenUrl } from '@/lib/deepgram-listen-url'; import { useRowboatAccount } from '@/hooks/useRowboatAccount'; +import posthog from 'posthog-js'; +import * as analytics from '@/lib/analytics'; export type VoiceState = 'idle' | 'connecting' | 'listening'; @@ -146,6 +148,8 @@ export function useVoiceMode() { // Show listening immediately — don't wait for WebSocket setState('listening'); + analytics.voiceInputStarted(); + posthog.people.set_once({ has_used_voice: true }); // Kick off mic + WebSocket in parallel, don't await WebSocket const [stream] = await Promise.all([ diff --git a/apps/x/apps/renderer/src/lib/analytics.ts b/apps/x/apps/renderer/src/lib/analytics.ts new file mode 100644 index 00000000..672ea0c3 --- /dev/null +++ b/apps/x/apps/renderer/src/lib/analytics.ts @@ -0,0 +1,37 @@ +import posthog from 'posthog-js' + +export function chatSessionCreated(runId: string) { + posthog.capture('chat_session_created', { run_id: runId }) +} + +export function chatMessageSent(props: { + voiceInput?: boolean + voiceOutput?: string + searchEnabled?: boolean +}) { + posthog.capture('chat_message_sent', { + voice_input: props.voiceInput ?? false, + voice_output: props.voiceOutput ?? false, + search_enabled: props.searchEnabled ?? false, + }) +} + +export function oauthConnected(provider: string) { + posthog.capture('oauth_connected', { provider }) +} + +export function oauthDisconnected(provider: string) { + posthog.capture('oauth_disconnected', { provider }) +} + +export function voiceInputStarted() { + posthog.capture('voice_input_started') +} + +export function searchExecuted(types: string[]) { + posthog.capture('search_executed', { types }) +} + +export function noteExported(format: string) { + posthog.capture('note_exported', { format }) +} diff --git a/apps/x/packages/core/src/application/assistant/instructions.ts b/apps/x/packages/core/src/application/assistant/instructions.ts index 3f923b38..76472c90 100644 --- a/apps/x/packages/core/src/application/assistant/instructions.ts +++ b/apps/x/packages/core/src/application/assistant/instructions.ts @@ -70,7 +70,6 @@ Rowboat is an agentic assistant for everyday work - emails, meetings, projects, **App Control:** When users ask you to open notes, show the bases or graph view, filter or search notes, or manage saved views, load the \`app-navigation\` skill first. It provides structured guidance for navigating the app UI and controlling the knowledge base view. -**Slack:** When users ask about Slack messages, want to send messages to teammates, check channel conversations, or find someone on Slack, load the \`slack\` skill. You can send messages, view channel history, search conversations, and find users. Always show message drafts to the user before sending. ## Learning About the User (save-to-memory) diff --git a/apps/x/packages/core/src/application/assistant/skills/index.ts b/apps/x/packages/core/src/application/assistant/skills/index.ts index ee51ecb3..84460961 100644 --- a/apps/x/packages/core/src/application/assistant/skills/index.ts +++ b/apps/x/packages/core/src/application/assistant/skills/index.ts @@ -7,7 +7,6 @@ import draftEmailsSkill from "./draft-emails/skill.js"; import mcpIntegrationSkill from "./mcp-integration/skill.js"; import meetingPrepSkill from "./meeting-prep/skill.js"; import organizeFilesSkill from "./organize-files/skill.js"; -import slackSkill from "./slack/skill.js"; import backgroundAgentsSkill from "./background-agents/skill.js"; import createPresentationsSkill from "./create-presentations/skill.js"; @@ -61,12 +60,6 @@ const definitions: SkillDefinition[] = [ summary: "Find, organize, and tidy up files on the user's machine. Move files to folders, clean up Desktop/Downloads, locate specific files.", content: organizeFilesSkill, }, - { - id: "slack", - title: "Slack Integration", - summary: "Send Slack messages, view channel history, search conversations, find users, and manage team communication.", - content: slackSkill, - }, { id: "background-agents", title: "Background Agents", diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 7e32a4fc..daf49b95 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -251,6 +251,7 @@ const ipcSchemas = { config: z.record(z.string(), z.object({ connected: z.boolean(), error: z.string().nullable().optional(), + userId: z.string().optional(), })), }), }, @@ -267,6 +268,7 @@ const ipcSchemas = { provider: z.string(), success: z.boolean(), error: z.string().optional(), + userId: z.string().optional(), }), res: z.null(), },