Merge branch 'dev' of github.com:rowboatlabs/rowboat into dev

This commit is contained in:
tusharmagar 2026-01-20 14:44:15 +05:30
commit c0efa3329a
10 changed files with 476 additions and 41 deletions

View file

@ -18,6 +18,7 @@ import z from 'zod';
import { RunEvent } from 'packages/shared/dist/runs.js';
import container from '@x/core/dist/di/container.js';
import { IGranolaConfigRepo } from '@x/core/dist/knowledge/granola/repo.js';
import { triggerSync as triggerGranolaSync } from '@x/core/dist/knowledge/granola/sync.js';
type InvokeChannels = ipc.InvokeChannels;
type IPCChannels = ipc.IPCChannels;
@ -316,6 +317,12 @@ export function setupIpcHandlers() {
'granola:setConfig': async (_event, args) => {
const repo = container.resolve<IGranolaConfigRepo>('granolaConfigRepo');
await repo.setConfig({ enabled: args.enabled });
// Trigger sync immediately when enabled
if (args.enabled) {
triggerGranolaSync();
}
return { success: true };
},
});

View file

@ -1,4 +1,4 @@
import { app, BrowserWindow, protocol, net } from "electron";
import { app, BrowserWindow, protocol, net, shell } from "electron";
import path from "node:path";
import { setupIpcHandlers, startRunsWatcher, startWorkspaceWatcher, stopWorkspaceWatcher } from "./ipc.js";
import { fileURLToPath, pathToFileURL } from "node:url";
@ -80,6 +80,24 @@ function createWindow() {
},
});
// Open external links in system browser (not sandboxed Electron window)
// This handles window.open() and target="_blank" links
win.webContents.setWindowOpenHandler(({ url }) => {
// Open all URLs in system browser
shell.openExternal(url);
return { action: 'deny' }; // Prevent Electron from opening a new window
});
// Handle navigation to external URLs (e.g., clicking a link without target="_blank")
win.webContents.on('will-navigate', (event, url) => {
// Allow internal navigation (app protocol or dev server)
const isInternal = url.startsWith('app://') || url.startsWith('http://localhost:5173');
if (!isInternal) {
event.preventDefault();
shell.openExternal(url);
}
});
// #region agent log
const loadURL = app.isPackaged ? 'app://./' : 'http://localhost:5173';
fetch('http://127.0.0.1:7242/ingest/dd33b297-24f6-4846-82f9-02599308a13a',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({location:'main.ts:65',message:'createWindow called',data:{isPackaged:app.isPackaged,loadURL,preloadPath},timestamp:Date.now(),sessionId:'debug-session',runId:'run1',hypothesisId:'E'})}).catch(()=>{});

View file

@ -1,4 +1,4 @@
import { BrowserWindow } from 'electron';
import { shell } from 'electron';
import { createAuthServer } from './auth-server.js';
import * as oauthClient from '@x/core/dist/auth/oauth-client.js';
import type { Configuration } from '@x/core/dist/auth/oauth-client.js';
@ -6,6 +6,9 @@ import { getProviderConfig, getAvailableProviders } from '@x/core/dist/auth/prov
import container from '@x/core/dist/di/container.js';
import { IOAuthRepo } from '@x/core/dist/auth/repo.js';
import { IClientRegistrationRepo } from '@x/core/dist/auth/client-repo.js';
import { triggerSync as triggerGmailSync } from '@x/core/dist/knowledge/sync_gmail.js';
import { triggerSync as triggerCalendarSync } from '@x/core/dist/knowledge/sync_calendar.js';
import { triggerSync as triggerFirefliesSync } from '@x/core/dist/knowledge/sync_fireflies.js';
const REDIRECT_URI = 'http://localhost:8080/oauth/callback';
@ -110,6 +113,17 @@ export async function connectProvider(provider: string): Promise<{ success: bool
// Store flow state
activeFlows.set(state, { codeVerifier, provider, config });
// Build authorization URL
const authUrl = oauthClient.buildAuthorizationUrl(config, {
redirectUri: REDIRECT_URI,
scope: scopes.join(' '),
codeChallenge,
state,
});
// Declare timeout variable (will be set after server is created)
let cleanupTimeout: NodeJS.Timeout;
// Create callback server
const { server } = await createAuthServer(8080, async (code, receivedState) => {
// Validate state
@ -138,6 +152,14 @@ export async function connectProvider(provider: string): Promise<{ success: bool
// Save tokens
console.log(`[OAuth] Token exchange successful for ${provider}`);
await oauthRepo.saveTokens(provider, tokens);
// Trigger immediate sync for relevant providers
if (provider === 'google') {
triggerGmailSync();
triggerCalendarSync();
} else if (provider === 'fireflies-ai') {
triggerFirefliesSync();
}
} catch (error) {
console.error('OAuth token exchange failed:', error);
throw error;
@ -145,35 +167,22 @@ export async function connectProvider(provider: string): Promise<{ success: bool
// Clean up
activeFlows.delete(state);
server.close();
clearTimeout(cleanupTimeout);
}
});
// Build authorization URL
const authUrl = oauthClient.buildAuthorizationUrl(config, {
redirectUri: REDIRECT_URI,
scope: scopes.join(' '),
codeChallenge,
state,
});
// Set timeout to clean up abandoned flows (5 minutes)
// This prevents memory leaks if user never completes the OAuth flow
cleanupTimeout = setTimeout(() => {
if (activeFlows.has(state)) {
console.log(`[OAuth] Cleaning up abandoned OAuth flow for ${provider} (timeout)`);
activeFlows.delete(state);
server.close();
}
}, 5 * 60 * 1000); // 5 minutes
// Open browser window
const authWindow = new BrowserWindow({
width: 600,
height: 700,
show: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
},
});
authWindow.loadURL(authUrl.toString());
// Clean up on window close
authWindow.on('closed', () => {
activeFlows.delete(state);
server.close();
});
// Open in system browser (shares cookies/sessions with user's regular browser)
shell.openExternal(authUrl.toString());
// Wait for callback (server will handle it)
return { success: true };