From 0185433986d2e041b57a4a78acbc218fbacd3634 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:59:32 +0530 Subject: [PATCH] add lookback period and note strictness dropdowns to onboarding Add two preference dropdowns to the Connected Accounts step: - Lookback period (1 week / 1 month / 3 months) for Gmail and Fireflies sync - Note strictness (Auto / High / Medium / Low) with inline descriptions Replaces hardcoded LOOKBACK_DAYS in sync_gmail and sync_fireflies with configurable value from ~/.rowboat/config/lookback.json. Adds IPC channels for reading/writing both configs. SelectItem now supports a description prop rendered only in the dropdown, not the trigger. Co-Authored-By: Claude Opus 4.6 --- apps/x/apps/main/src/ipc.ts | 25 ++++- .../src/components/onboarding-modal.tsx | 106 +++++++++++++++++- .../renderer/src/components/ui/select.tsx | 8 +- .../core/src/config/lookback_config.ts | 44 ++++++++ .../core/src/config/note_creation_config.ts | 11 ++ .../core/src/knowledge/sync_fireflies.ts | 9 +- .../packages/core/src/knowledge/sync_gmail.ts | 3 +- apps/x/packages/shared/src/ipc.ts | 35 ++++++ 8 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 apps/x/packages/core/src/config/lookback_config.ts diff --git a/apps/x/apps/main/src/ipc.ts b/apps/x/apps/main/src/ipc.ts index b0757881..1553887a 100644 --- a/apps/x/apps/main/src/ipc.ts +++ b/apps/x/apps/main/src/ipc.ts @@ -25,7 +25,8 @@ import type { IModelConfigRepo } from '@x/core/dist/models/repo.js'; import type { IOAuthRepo } from '@x/core/dist/auth/repo.js'; import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js'; import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js'; -import { isOnboardingComplete, markOnboardingComplete } from '@x/core/dist/config/note_creation_config.js'; +import { isOnboardingComplete, markOnboardingComplete, getNoteCreationStrictness, isStrictnessConfigured, setStrictnessAndMarkConfigured, resetStrictnessToAuto } from '@x/core/dist/config/note_creation_config.js'; +import { getLookbackDays, setLookbackDays } from '@x/core/dist/config/lookback_config.js'; import * as composioHandler from './composio-handler.js'; import { IAgentScheduleRepo } from '@x/core/dist/agent-schedule/repo.js'; import { IAgentScheduleStateRepo } from '@x/core/dist/agent-schedule/state-repo.js'; @@ -460,6 +461,28 @@ export function setupIpcHandlers() { await stateRepo.deleteAgentState(args.agentName); return { success: true }; }, + // Config handlers + 'config:getLookback': async () => { + return { days: getLookbackDays() }; + }, + 'config:setLookback': async (_event, args) => { + setLookbackDays(args.days); + return { success: true }; + }, + 'config:getNoteStrictness': async () => { + return { + strictness: getNoteCreationStrictness(), + configured: isStrictnessConfigured(), + }; + }, + 'config:setNoteStrictness': async (_event, args) => { + setStrictnessAndMarkConfigured(args.strictness); + return { success: true }; + }, + 'config:resetNoteStrictness': async () => { + resetStrictnessToAuto(); + return { success: true }; + }, // Shell integration handlers 'shell:openPath': async (_event, args) => { let filePath = args.path; diff --git a/apps/x/apps/renderer/src/components/onboarding-modal.tsx b/apps/x/apps/renderer/src/components/onboarding-modal.tsx index 4855cab7..7358f6b2 100644 --- a/apps/x/apps/renderer/src/components/onboarding-modal.tsx +++ b/apps/x/apps/renderer/src/components/onboarding-modal.tsx @@ -80,6 +80,12 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { const [granolaLoading, setGranolaLoading] = useState(true) const [showMoreProviders, setShowMoreProviders] = useState(false) + // Lookback period state + const [lookbackDays, setLookbackDays] = useState<7 | 30 | 90>(30) + + // Note strictness state + const [noteStrictness, setNoteStrictness] = useState<"auto" | "high" | "medium" | "low">("auto") + // Composio/Slack state const [composioApiKeyOpen, setComposioApiKeyOpen] = useState(false) const [slackConnected, setSlackConnected] = useState(false) @@ -183,6 +189,50 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { }) }, [modelsCatalog]) + // Load lookback and note strictness config on mount + useEffect(() => { + if (!open) return + async function loadConfigs() { + try { + const lookback = await window.ipc.invoke('config:getLookback', null) + setLookbackDays(lookback.days) + } catch (error) { + console.error('Failed to load lookback config:', error) + } + // Note strictness always defaults to "Auto" in onboarding — + // the user makes their explicit choice here. + } + loadConfigs() + }, [open]) + + // Handle lookback period change + const handleLookbackChange = useCallback(async (value: string) => { + const days = Number(value) as 7 | 30 | 90 + setLookbackDays(days) + try { + await window.ipc.invoke('config:setLookback', { days }) + } catch (error) { + console.error('Failed to save lookback config:', error) + toast.error('Failed to save lookback period') + } + }, []) + + // Handle note strictness change + const handleNoteStrictnessChange = useCallback(async (value: string) => { + const strictness = value as "auto" | "high" | "medium" | "low" + setNoteStrictness(strictness) + try { + if (strictness === "auto") { + await window.ipc.invoke('config:resetNoteStrictness', null) + } else { + await window.ipc.invoke('config:setNoteStrictness', { strictness }) + } + } catch (error) { + console.error('Failed to save note strictness config:', error) + toast.error('Failed to save note strictness') + } + }, []) + // Load Granola config const refreshGranolaConfig = useCallback(async () => { try { @@ -270,7 +320,19 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { } }, [startSlackConnect]) - const handleNext = () => { + const handleNext = async () => { + // Save preferences when leaving the connected accounts step + if (currentStep === 1) { + try { + if (noteStrictness === "auto") { + await window.ipc.invoke('config:resetNoteStrictness', null) + } else { + await window.ipc.invoke('config:setNoteStrictness', { strictness: noteStrictness }) + } + } catch (error) { + console.error('Failed to save note strictness config:', error) + } + } if (currentStep < 2) { setCurrentStep((prev) => (prev + 1) as Step) } @@ -783,6 +845,48 @@ export function OnboardingModal({ open, onComplete }: OnboardingModalProps) { {providers.includes('fireflies-ai') && renderOAuthProvider('fireflies-ai', 'Fireflies', , 'AI meeting transcripts')} + {/* Preferences Section */} +
+
+ Preferences +
+
+
+
+ Lookback period + How far back to sync emails and meetings +
+ +
+
+
+ Note strictness + Controls what qualifies for creating a note +
+ +
+
+
+ )} diff --git a/apps/x/apps/renderer/src/components/ui/select.tsx b/apps/x/apps/renderer/src/components/ui/select.tsx index 88302a8d..d7495520 100644 --- a/apps/x/apps/renderer/src/components/ui/select.tsx +++ b/apps/x/apps/renderer/src/components/ui/select.tsx @@ -103,8 +103,11 @@ function SelectLabel({ function SelectItem({ className, children, + description, ...props -}: React.ComponentProps) { +}: React.ComponentProps & { + description?: string +}) { return ( {children} + {description && ( + {description} + )} ) } diff --git a/apps/x/packages/core/src/config/lookback_config.ts b/apps/x/packages/core/src/config/lookback_config.ts new file mode 100644 index 00000000..bfe7ea44 --- /dev/null +++ b/apps/x/packages/core/src/config/lookback_config.ts @@ -0,0 +1,44 @@ +import fs from 'fs'; +import path from 'path'; +import { WorkDir } from './config.js'; + +export type LookbackDays = 7 | 30 | 90; + +interface LookbackConfig { + days: LookbackDays; +} + +const CONFIG_FILE = path.join(WorkDir, 'config', 'lookback.json'); +const DEFAULT_DAYS: LookbackDays = 30; +const VALID_VALUES: LookbackDays[] = [7, 30, 90]; + +function readConfig(): LookbackConfig { + try { + if (!fs.existsSync(CONFIG_FILE)) { + return { days: DEFAULT_DAYS }; + } + const raw = fs.readFileSync(CONFIG_FILE, 'utf-8'); + const config = JSON.parse(raw); + return { + days: VALID_VALUES.includes(config.days) ? config.days : DEFAULT_DAYS, + }; + } catch { + return { days: DEFAULT_DAYS }; + } +} + +function writeConfig(config: LookbackConfig): void { + const configDir = path.dirname(CONFIG_FILE); + if (!fs.existsSync(configDir)) { + fs.mkdirSync(configDir, { recursive: true }); + } + fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); +} + +export function getLookbackDays(): LookbackDays { + return readConfig().days; +} + +export function setLookbackDays(days: LookbackDays): void { + writeConfig({ days }); +} diff --git a/apps/x/packages/core/src/config/note_creation_config.ts b/apps/x/packages/core/src/config/note_creation_config.ts index a86e8c00..e77527bb 100644 --- a/apps/x/packages/core/src/config/note_creation_config.ts +++ b/apps/x/packages/core/src/config/note_creation_config.ts @@ -90,6 +90,17 @@ export function setStrictnessAndMarkConfigured(strictness: NoteCreationStrictnes writeConfig(config); } +/** + * Reset strictness to default and mark as not configured, + * allowing auto-analysis to run again. + */ +export function resetStrictnessToAuto(): void { + const config = readConfig(); + config.strictness = DEFAULT_STRICTNESS; + config.configured = false; + writeConfig(config); +} + /** * Get the agent file name suffix based on strictness. */ diff --git a/apps/x/packages/core/src/knowledge/sync_fireflies.ts b/apps/x/packages/core/src/knowledge/sync_fireflies.ts index 5e0cca07..6cc93d31 100644 --- a/apps/x/packages/core/src/knowledge/sync_fireflies.ts +++ b/apps/x/packages/core/src/knowledge/sync_fireflies.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import path from 'path'; import { WorkDir } from '../config/config.js'; +import { getLookbackDays } from '../config/lookback_config.js'; import { FirefliesClientFactory } from './fireflies-client-factory.js'; import { serviceLogger, type ServiceRunContext } from '../services/service_logger.js'; import { limitEventItems } from './limit_event_items.js'; @@ -9,7 +10,6 @@ import { limitEventItems } from './limit_event_items.js'; const SYNC_DIR = path.join(WorkDir, 'fireflies_transcripts'); const SYNC_INTERVAL_MS = 30 * 60 * 1000; // Check every 30 minutes (reduced from 1 minute) const STATE_FILE = path.join(SYNC_DIR, 'sync_state.json'); -const LOOKBACK_DAYS = 30; // Last 1 month const API_DELAY_MS = 2000; // 2 second delay between API calls const RATE_LIMIT_RETRY_DELAY_MS = 60 * 1000; // Wait 1 minute on rate limit const MAX_RETRIES = 3; // Maximum retries for rate-limited requests @@ -406,10 +406,11 @@ async function syncMeetings() { } } - // Calculate date range (last 30 days) + // Calculate date range based on configured lookback + const lookbackDays = getLookbackDays(); const toDate = new Date(); const fromDate = new Date(); - fromDate.setDate(fromDate.getDate() - LOOKBACK_DAYS); + fromDate.setDate(fromDate.getDate() - lookbackDays); const fromDateStr = fromDate.toISOString().split('T')[0]; // YYYY-MM-DD const toDateStr = toDate.toISOString().split('T')[0]; @@ -637,7 +638,7 @@ async function syncMeetings() { export async function init() { console.log('[Fireflies] Starting Fireflies Sync...'); console.log(`[Fireflies] Will sync every ${SYNC_INTERVAL_MS / 1000} seconds.`); - console.log(`[Fireflies] Syncing transcripts from the last ${LOOKBACK_DAYS} days.`); + console.log(`[Fireflies] Syncing transcripts from the last ${getLookbackDays()} days.`); while (true) { try { diff --git a/apps/x/packages/core/src/knowledge/sync_gmail.ts b/apps/x/packages/core/src/knowledge/sync_gmail.ts index de73c016..dcb7bc3b 100644 --- a/apps/x/packages/core/src/knowledge/sync_gmail.ts +++ b/apps/x/packages/core/src/knowledge/sync_gmail.ts @@ -4,6 +4,7 @@ import { google, gmail_v1 as gmail } from 'googleapis'; import { NodeHtmlMarkdown } from 'node-html-markdown' import { OAuth2Client } from 'google-auth-library'; import { WorkDir } from '../config/config.js'; +import { getLookbackDays } from '../config/lookback_config.js'; import { GoogleClientFactory } from './google-client-factory.js'; import { serviceLogger, type ServiceRunContext } from '../services/service_logger.js'; import { limitEventItems } from './limit_event_items.js'; @@ -408,7 +409,7 @@ async function partialSync(auth: OAuth2Client, startHistoryId: string, syncDir: } async function performSync() { - const LOOKBACK_DAYS = 30; // Default to 1 month + const LOOKBACK_DAYS = getLookbackDays(); const ATTACHMENTS_DIR = path.join(SYNC_DIR, 'attachments'); const STATE_FILE = path.join(SYNC_DIR, 'sync_state.json'); diff --git a/apps/x/packages/shared/src/ipc.ts b/apps/x/packages/shared/src/ipc.ts index 1fa9b423..e5062b8c 100644 --- a/apps/x/packages/shared/src/ipc.ts +++ b/apps/x/packages/shared/src/ipc.ts @@ -388,6 +388,41 @@ const ipcSchemas = { }), }, // Shell integration channels + 'config:getLookback': { + req: z.null(), + res: z.object({ + days: z.union([z.literal(7), z.literal(30), z.literal(90)]), + }), + }, + 'config:setLookback': { + req: z.object({ + days: z.union([z.literal(7), z.literal(30), z.literal(90)]), + }), + res: z.object({ + success: z.literal(true), + }), + }, + 'config:getNoteStrictness': { + req: z.null(), + res: z.object({ + strictness: z.enum(['low', 'medium', 'high']), + configured: z.boolean(), + }), + }, + 'config:setNoteStrictness': { + req: z.object({ + strictness: z.enum(['low', 'medium', 'high']), + }), + res: z.object({ + success: z.literal(true), + }), + }, + 'config:resetNoteStrictness': { + req: z.null(), + res: z.object({ + success: z.literal(true), + }), + }, 'shell:openPath': { req: z.object({ path: z.string() }), res: z.object({ error: z.string().optional() }),