diff --git a/apps/x/apps/main/src/auth-server.ts b/apps/x/apps/main/src/auth-server.ts index 5c46ca3f..ad184451 100644 --- a/apps/x/apps/main/src/auth-server.ts +++ b/apps/x/apps/main/src/auth-server.ts @@ -2,8 +2,7 @@ import { createServer, Server } from 'http'; import { URL } from 'url'; const OAUTH_CALLBACK_PATH = '/oauth/callback'; -export const DEFAULT_PORT = 8080; -export const PORT_RANGE_SIZE = 10; +const DEFAULT_PORT = 8080; /** Escape HTML special characters to prevent XSS */ function escapeHtml(str: string): string { @@ -20,8 +19,13 @@ export interface AuthServerResult { port: number; } -function tryBindPort( - port: number, +/** + * Create a local HTTP server to handle OAuth callback + * Listens on http://localhost:8080/oauth/callback + * Passes the full callback URL (including iss, scope, etc.) so openid-client validation succeeds. + */ +export function createAuthServer( + port: number = DEFAULT_PORT, onCallback: (callbackUrl: URL) => void | Promise ): Promise { return new Promise((resolve, reject) => { @@ -33,7 +37,7 @@ function tryBindPort( } const url = new URL(req.url, `http://localhost:${port}`); - + if (url.pathname === OAUTH_CALLBACK_PATH) { const error = url.searchParams.get('error'); @@ -92,10 +96,8 @@ function tryBindPort( }); server.on('error', (err: NodeJS.ErrnoException) => { - server.close(); - if (err.code === 'EADDRINUSE' || err.code === 'EACCES') { - // Signal caller to try next port - reject(Object.assign(new Error(err.code), { code: err.code })); + if (err.code === 'EADDRINUSE') { + reject(new Error(`Port ${port} is already in use`)); } else { reject(err); } @@ -103,51 +105,3 @@ function tryBindPort( }); } -/** - * Create a local HTTP server to handle OAuth callback. - * - * Defaults to fixed-port behaviour: only `port` is tried, and a clear error is - * thrown if it cannot be bound. This is the right behaviour for any provider - * whose redirect URI is pre-registered (Google BYOK, Composio, etc.) — those - * callers must keep using the exact port they've handed to the provider. - * - * Opt into `{ fallback: true }` only when the caller is prepared to use the - * port returned in `AuthServerResult` (i.e. the redirect URI is built from the - * actual bound port, not hard-coded). With fallback enabled, scans `port` - * through `port + PORT_RANGE_SIZE - 1` and binds the first available, handling - * both EADDRINUSE and EACCES (the latter is common on Windows when - * Hyper-V/WSL2 reserve the port). - */ -export async function createAuthServer( - port: number = DEFAULT_PORT, - onCallback: (callbackUrl: URL) => void | Promise, - opts: { fallback?: boolean } = {}, -): Promise { - const fallback = opts.fallback === true; - const limit = fallback ? port + PORT_RANGE_SIZE - 1 : port; - - for (let p = port; p <= limit; p++) { - try { - return await tryBindPort(p, onCallback); - } catch (err) { - const code = (err as NodeJS.ErrnoException).code; - if (fallback && (code === 'EADDRINUSE' || code === 'EACCES') && p < limit) { - console.warn(`[OAuth] Port ${p} unavailable (${code}), trying ${p + 1}…`); - continue; - } - if (!fallback) { - const reason = code === 'EACCES' || code === 'EADDRINUSE' - ? `Port ${port} is unavailable (${code}). This port must be free for sign-in to work — close any app using it and try again.` - : (err instanceof Error ? err.message : String(err)); - throw new Error(reason); - } - throw new Error( - `No available port found in range ${port}–${limit}. Free a port in that range and try again.` - ); - } - } - - // Unreachable — loop always returns or throws — but satisfies TypeScript - throw new Error(`No available port found in range ${port}–${limit}.`); -} - diff --git a/apps/x/apps/main/src/oauth-handler.ts b/apps/x/apps/main/src/oauth-handler.ts index ab00ab8c..f61b59cc 100644 --- a/apps/x/apps/main/src/oauth-handler.ts +++ b/apps/x/apps/main/src/oauth-handler.ts @@ -1,7 +1,6 @@ import { shell } from 'electron'; import type { Server } from 'http'; import { createAuthServer } from './auth-server.js'; -import { DEFAULT_CALLBACK_PORT } from '@x/core/dist/auth/client-repo.js'; import * as oauthClient from '@x/core/dist/auth/oauth-client.js'; import type { Configuration } from '@x/core/dist/auth/oauth-client.js'; import { getProviderConfig, getAvailableProviders } from '@x/core/dist/auth/providers.js'; @@ -18,9 +17,7 @@ import { isSignedIn } from '@x/core/dist/account/account.js'; import { getWebappUrl } from '@x/core/dist/config/remote-config.js'; import { claimTokensViaBackend } from '@x/core/dist/auth/google-backend-oauth.js'; -function buildRedirectUri(port: number): string { - return `http://localhost:${port}/oauth/callback`; -} +const REDIRECT_URI = 'http://localhost:8080/oauth/callback'; /** Top-level openid-client messages that often wrap a more specific cause. */ const OPAQUE_OAUTH_TOP_MESSAGES = new Set(['invalid response encountered']); @@ -117,15 +114,9 @@ function getClientRegistrationRepo(): IClientRegistrationRepo { } /** - * Get or create OAuth configuration for a provider. - * `redirectUri` is required for DCR providers — it is the actual callback URI - * (including port) that was just bound, so the registration and auth URL stay in sync. + * Get or create OAuth configuration for a provider */ -async function getProviderConfiguration( - provider: string, - redirectUri: string = buildRedirectUri(DEFAULT_CALLBACK_PORT), - credentialsOverride?: { clientId: string; clientSecret: string }, -): Promise { +async function getProviderConfiguration(provider: string, credentialsOverride?: { clientId: string; clientSecret: string }): Promise { const config = await getProviderConfig(provider); const resolveClientCredentials = async (): Promise<{ clientId: string; clientSecret?: string }> => { if (config.client.mode === 'static' && config.client.clientId) { @@ -157,7 +148,7 @@ async function getProviderConfiguration( console.log(`[OAuth] ${provider}: Discovery from issuer with DCR`); const clientRepo = getClientRegistrationRepo(); const existingRegistration = await clientRepo.getClientRegistration(provider); - + if (existingRegistration) { console.log(`[OAuth] ${provider}: Using existing DCR registration`); return await oauthClient.discoverConfiguration( @@ -166,21 +157,18 @@ async function getProviderConfiguration( ); } - // Register new client with the actual redirect URI (port already bound) + // Register new client const scopes = config.scopes || []; const { config: oauthConfig, registration } = await oauthClient.registerClient( config.discovery.issuer, - [redirectUri], + [REDIRECT_URI], scopes ); - - // Parse port from redirectUri (e.g. "http://localhost:8081/...") and save - const boundPort = new URL(redirectUri).port - ? parseInt(new URL(redirectUri).port, 10) - : DEFAULT_CALLBACK_PORT; - await clientRepo.saveClientRegistration(provider, registration, boundPort); - console.log(`[OAuth] ${provider}: DCR registration saved (port ${boundPort})`); - + + // Save registration for future use + await clientRepo.saveClientRegistration(provider, registration); + console.log(`[OAuth] ${provider}: DCR registration saved`); + return oauthConfig; } } else { @@ -188,7 +176,7 @@ async function getProviderConfiguration( if (config.client.mode !== 'static') { throw new Error('DCR requires discovery mode "issuer", not "static"'); } - + console.log(`[OAuth] ${provider}: Using static endpoints (no discovery)`); const { clientId, clientSecret } = await resolveClientCredentials(); return oauthClient.createStaticConfiguration( @@ -201,37 +189,6 @@ async function getProviderConfiguration( } } -/** - * Determine which port to start the OAuth callback server on for a DCR provider. - * - * If the provider has an existing registration, probes the port it was registered - * on. If that port is still available, returns it so the existing client_id keeps - * working. If it is blocked, clears the stale registration (forcing re-registration - * on the next available port) and returns DEFAULT_CALLBACK_PORT as the scan base. - * - * Exported for unit testing. - */ -export async function resolveStartPort( - provider: string, - clientRepo: IClientRegistrationRepo, -): Promise { - const existingReg = await clientRepo.getClientRegistration(provider); - if (!existingReg) return DEFAULT_CALLBACK_PORT; - - const registeredPort = await clientRepo.getRegisteredPort(provider); - try { - // Probe — fixed-port (no fallback) so we know whether the exact registered port is free - const probe = await createAuthServer(registeredPort, () => { /* probe */ }); - probe.server.close(); - console.log(`[OAuth] ${provider}: registered port ${registeredPort} still available`); - return registeredPort; - } catch { - console.log(`[OAuth] ${provider}: registered port ${registeredPort} blocked, clearing DCR registration`); - await clientRepo.clearClientRegistration(provider); - return DEFAULT_CALLBACK_PORT; - } -} - /** * Initiate OAuth flow for a provider */ @@ -268,188 +225,154 @@ export async function connectProvider(provider: string, credentials?: { clientId } } - // For static-client providers (Google BYOK) the redirect URI is pre-registered - // at the OAuth provider console on a fixed port — we must not scan. - // For DCR providers, resolveStartPort handles the re-registration trap. - const isStaticClient = providerConfig.client.mode === 'static'; - const startPort = isStaticClient - ? DEFAULT_CALLBACK_PORT - : await resolveStartPort(provider, getClientRegistrationRepo()); + // Get or create OAuth configuration + const config = await getProviderConfiguration(provider, credentials); - // --- Callback server --- - // Declare `state` before the closure so the callback can close over its binding. - // The variable is assigned below, before shell.openExternal, so it is always - // set by the time any browser request arrives. - let state = ''; + // Generate PKCE codes + const { verifier: codeVerifier, challenge: codeChallenge } = await oauthClient.generatePKCE(); + const state = oauthClient.generateState(); + + // Get scopes from config + const scopes = providerConfig.scopes || []; + + // Store flow state + activeFlows.set(state, { codeVerifier, provider, config }); + + // Build authorization URL + const authUrl = oauthClient.buildAuthorizationUrl(config, { + redirect_uri: REDIRECT_URI, + scope: scopes.join(' '), + code_challenge: codeChallenge, + state, + }); + + // Create callback server let callbackHandled = false; - - const { server, port: boundPort } = await createAuthServer( - startPort, - async (callbackUrl) => { - // Guard against duplicate callbacks (browser may send multiple requests) - if (callbackHandled) return; - callbackHandled = true; - const receivedState = callbackUrl.searchParams.get('state'); - if (receivedState == null || receivedState === '') { - throw new Error( - 'OAuth callback missing state parameter. Complete sign-in in the browser or check the redirect URI.' - ); - } - if (receivedState !== state) { - throw new Error('Invalid state parameter - possible CSRF attack'); - } - - const flow = activeFlows.get(state); - if (!flow || flow.provider !== provider) { - throw new Error('Invalid OAuth flow state'); - } - - try { - // Use full callback URL (includes iss, scope, etc.) so openid-client validation succeeds - console.log(`[OAuth] Exchanging authorization code for tokens (${provider})...`); - const tokens = await oauthClient.exchangeCodeForTokens( - flow.config, - callbackUrl, - flow.codeVerifier, - state - ); - - // Save tokens and credentials. For Google, BYOK is the only path - // that reaches this token exchange (rowboat path returns above - // before any local server runs); stamp mode: 'byok' so a future - // refresh / reconnect can't get confused with a rowboat entry. - console.log(`[OAuth] Token exchange successful for ${provider}`); - await oauthRepo.upsert(provider, { - tokens, - ...(credentials ? { clientId: credentials.clientId, clientSecret: credentials.clientSecret } : {}), - ...(provider === 'google' ? { mode: 'byok' as const } : {}), - error: null, - }); - - // Trigger immediate sync for relevant providers - if (provider === 'google') { - triggerGmailSync(); - triggerCalendarSync(); - } else if (provider === 'fireflies-ai') { - triggerFirefliesSync(); - } - - // For Rowboat sign-in, ensure user + Stripe customer exist before - // notifying the renderer. Without this, parallel API calls from - // multiple renderer hooks race to create the user, causing duplicates. - let signedInUserId: string | undefined; - if (provider === 'rowboat') { - try { - const billing = await getBillingInfo(); - if (billing.userId) { - signedInUserId = billing.userId; - analyticsIdentify(billing.userId, { - ...(billing.userEmail ? { email: billing.userEmail } : {}), - plan: billing.subscriptionPlan, - status: billing.subscriptionStatus, - }); - analyticsCapture('user_signed_in', { - plan: billing.subscriptionPlan, - status: billing.subscriptionStatus, - }); - } - } catch (meError) { - console.error('[OAuth] Failed to initialize user via /v1/me:', meError); - } - } - - // Emit success event to renderer - emitOAuthEvent({ - provider, - success: true, - ...(signedInUserId ? { userId: signedInUserId } : {}), - }); - } catch (error) { - console.error('OAuth token exchange failed:', error); - // Log cause chain for debugging (e.g. OAUTH_INVALID_RESPONSE -> OperationProcessingError) - let cause: unknown = error; - while (cause != null && typeof cause === 'object' && 'cause' in cause) { - cause = (cause as { cause?: unknown }).cause; - if (cause != null) { - console.error('[OAuth] Caused by:', cause); - } - } - const errorMessage = getOAuthErrorMessage(error); - emitOAuthEvent({ provider, success: false, error: errorMessage }); - throw error; - } finally { - // Clean up - activeFlows.delete(state); - if (activeFlow && activeFlow.state === state) { - clearTimeout(activeFlow.cleanupTimeout); - activeFlow.server.close(); - activeFlow = null; - } - } - }, - // Static providers (Google BYOK) keep fixed-port behaviour to match the - // pre-registered redirect URI at the provider's console. DCR providers - // can fall back since we register the actual bound port below. - { fallback: !isStaticClient }, - ); - - // Server is bound. Any throw between here and `activeFlow = ...` would - // leak the port — `cancelActiveFlow` only closes it once activeFlow is set. - try { - // TOCTOU guard: resolveStartPort probed the registered port and found it - // free, but the port could have been grabbed between probe and real bind, - // causing fallback to a different port. The cached client_id is registered - // for the old port — clear it so getProviderConfiguration re-registers - // with the actual bound port. - if (!isStaticClient && boundPort !== startPort) { - console.log(`[OAuth] ${provider}: bound port ${boundPort} differs from start port ${startPort}, clearing stale DCR registration`); - await getClientRegistrationRepo().clearClientRegistration(provider); + const { server } = await createAuthServer(8080, async (callbackUrl) => { + // Guard against duplicate callbacks (browser may send multiple requests) + if (callbackHandled) return; + callbackHandled = true; + const receivedState = callbackUrl.searchParams.get('state'); + if (receivedState == null || receivedState === '') { + throw new Error( + 'OAuth callback missing state parameter. Complete sign-in in the browser or check the redirect URI.' + ); + } + if (receivedState !== state) { + throw new Error('Invalid state parameter - possible CSRF attack'); } - const redirectUri = buildRedirectUri(boundPort); - const config = await getProviderConfiguration(provider, redirectUri, credentials); + const flow = activeFlows.get(state); + if (!flow || flow.provider !== provider) { + throw new Error('Invalid OAuth flow state'); + } - const { verifier: codeVerifier, challenge: codeChallenge } = await oauthClient.generatePKCE(); - state = oauthClient.generateState(); + try { + // Use full callback URL (includes iss, scope, etc.) so openid-client validation succeeds + console.log(`[OAuth] Exchanging authorization code for tokens (${provider})...`); + const tokens = await oauthClient.exchangeCodeForTokens( + flow.config, + callbackUrl, + flow.codeVerifier, + state + ); - const scopes = providerConfig.scopes || []; - activeFlows.set(state, { codeVerifier, provider, config }); + // Save tokens and credentials. For Google, BYOK is the only path + // that reaches this token exchange (rowboat path returns above + // before any local server runs); stamp mode: 'byok' so a future + // refresh / reconnect can't get confused with a rowboat entry. + console.log(`[OAuth] Token exchange successful for ${provider}`); + await oauthRepo.upsert(provider, { + tokens, + ...(credentials ? { clientId: credentials.clientId, clientSecret: credentials.clientSecret } : {}), + ...(provider === 'google' ? { mode: 'byok' as const } : {}), + error: null, + }); - const authUrl = oauthClient.buildAuthorizationUrl(config, { - redirect_uri: redirectUri, - scope: scopes.join(' '), - code_challenge: codeChallenge, - state, - }); - - // Set timeout to clean up abandoned flows (2 minutes) - const cleanupTimeout = setTimeout(() => { - if (activeFlow?.state === state) { - console.log(`[OAuth] Cleaning up abandoned OAuth flow for ${provider} (timeout)`); - cancelActiveFlow('timed_out'); + // Trigger immediate sync for relevant providers + if (provider === 'google') { + triggerGmailSync(); + triggerCalendarSync(); + } else if (provider === 'fireflies-ai') { + triggerFirefliesSync(); } - }, 2 * 60 * 1000); - activeFlow = { - provider, - state, - server, - cleanupTimeout, - }; + // For Rowboat sign-in, ensure user + Stripe customer exist before + // notifying the renderer. Without this, parallel API calls from + // multiple renderer hooks race to create the user, causing duplicates. + let signedInUserId: string | undefined; + if (provider === 'rowboat') { + try { + const billing = await getBillingInfo(); + if (billing.userId) { + signedInUserId = billing.userId; + analyticsIdentify(billing.userId, { + ...(billing.userEmail ? { email: billing.userEmail } : {}), + plan: billing.subscriptionPlan, + status: billing.subscriptionStatus, + }); + analyticsCapture('user_signed_in', { + plan: billing.subscriptionPlan, + status: billing.subscriptionStatus, + }); + } + } catch (meError) { + console.error('[OAuth] Failed to initialize user via /v1/me:', meError); + } + } - // Open in system browser (shares cookies/sessions with user's regular browser) - shell.openExternal(authUrl.toString()); - - return { success: true }; - } catch (setupError) { - // Post-bind setup failed — close the server so the port is released and - // a retry isn't blocked by our own zombie listener. - server.close(); - if (state) { + // Emit success event to renderer + emitOAuthEvent({ + provider, + success: true, + ...(signedInUserId ? { userId: signedInUserId } : {}), + }); + } catch (error) { + console.error('OAuth token exchange failed:', error); + // Log cause chain for debugging (e.g. OAUTH_INVALID_RESPONSE -> OperationProcessingError) + let cause: unknown = error; + while (cause != null && typeof cause === 'object' && 'cause' in cause) { + cause = (cause as { cause?: unknown }).cause; + if (cause != null) { + console.error('[OAuth] Caused by:', cause); + } + } + const errorMessage = getOAuthErrorMessage(error); + emitOAuthEvent({ provider, success: false, error: errorMessage }); + throw error; + } finally { + // Clean up activeFlows.delete(state); + if (activeFlow && activeFlow.state === state) { + clearTimeout(activeFlow.cleanupTimeout); + activeFlow.server.close(); + activeFlow = null; + } } - throw setupError; - } + }); + + // Set timeout to clean up abandoned flows (2 minutes) + // This prevents memory leaks if user never completes the OAuth flow + const cleanupTimeout = setTimeout(() => { + if (activeFlow?.state === state) { + console.log(`[OAuth] Cleaning up abandoned OAuth flow for ${provider} (timeout)`); + cancelActiveFlow('timed_out'); + } + }, 2 * 60 * 1000); // 2 minutes + + // Store complete flow state for cleanup + activeFlow = { + provider, + state, + server, + cleanupTimeout, + }; + + // 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 }; } catch (error) { console.error('OAuth connection failed:', error); return { diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index 4904a119..e6c050b3 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -5,12 +5,10 @@ import { RunEvent, ListRunsResponse } from '@x/shared/src/runs.js'; import type { LanguageModelUsage, ToolUIPart } from 'ai'; import './App.css' import z from 'zod'; -import { CheckIcon, LoaderIcon, PanelLeftIcon, ArrowRight, MessageSquare, ChevronLeftIcon, ChevronRightIcon, Plus, HistoryIcon } from 'lucide-react'; +import { CheckIcon, LoaderIcon, PanelLeftIcon, Maximize2, Minimize2, ChevronLeftIcon, ChevronRightIcon, SquarePen, HistoryIcon } from 'lucide-react'; import { cn } from '@/lib/utils'; import { MarkdownEditor, type MarkdownEditorHandle } from './components/markdown-editor'; import { ChatSidebar } from './components/chat-sidebar'; -import { ChatHeader } from './components/chat-header'; -import { ChatEmptyState } from './components/chat-empty-state'; import { ChatInputWithMentions, type StagedAttachment } from './components/chat-input-with-mentions'; import { ChatMessageAttachments } from '@/components/chat-message-attachments' import { GraphView, type GraphEdge, type GraphNode } from '@/components/graph-view'; @@ -27,15 +25,12 @@ import { SuggestedTopicsView } from '@/components/suggested-topics-view'; import { LiveNotesView } from '@/components/live-notes-view'; import { BgTasksView } from '@/components/bg-tasks-view'; import { EmailView } from '@/components/email-view'; -import { WorkspaceView } from '@/components/workspace-view'; -import { KnowledgeView } from '@/components/knowledge-view'; -import { ChatHistoryView } from '@/components/chat-history-view'; -import { HomeView } from '@/components/home-view'; import { MeetingsView } from '@/components/meetings-view'; import { SidebarSectionProvider } from '@/contexts/sidebar-context'; import { Conversation, ConversationContent, + ConversationEmptyState, ConversationScrollButton, } from '@/components/ai-elements/conversation'; import { @@ -57,6 +52,7 @@ import { ComposioConnectCard } from '@/components/ai-elements/composio-connect-c import { PermissionRequest } from '@/components/ai-elements/permission-request'; import { TerminalOutput } from '@/components/terminal-output'; import { AskHumanRequest } from '@/components/ai-elements/ask-human-request'; +import { Suggestions } from '@/components/ai-elements/suggestions'; import { ToolPermissionRequestEvent, AskHumanRequestEvent } from '@x/shared/src/runs.js'; import { SidebarInset, @@ -67,8 +63,6 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Toaster } from "@/components/ui/sonner" -import { BillingErrorDialog } from "@/components/billing-error-dialog" -import { matchBillingError, type BillingErrorMatch } from "@/lib/billing-error" import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links' import { splitFrontmatter, joinFrontmatter } from '@/lib/frontmatter' import { extractConferenceLink } from '@/lib/calendar-event' @@ -188,11 +182,6 @@ const MEETINGS_TAB_PATH = '__rowboat_meetings__' const LIVE_NOTES_TAB_PATH = '__rowboat_live_notes__' const BG_TASKS_TAB_PATH = '__rowboat_bg_tasks__' const EMAIL_TAB_PATH = '__rowboat_email__' -const WORKSPACE_TAB_PATH = '__rowboat_workspace__' -const WORKSPACE_ROOT = 'knowledge/Workspace' -const KNOWLEDGE_VIEW_TAB_PATH = '__rowboat_knowledge_view__' -const CHAT_HISTORY_TAB_PATH = '__rowboat_chat_history__' -const HOME_TAB_PATH = '__rowboat_home__' const BASES_DEFAULT_TAB_PATH = '__rowboat_bases_default__' const clampNumber = (value: number, min: number, max: number) => @@ -326,10 +315,6 @@ const isMeetingsTabPath = (path: string) => path === MEETINGS_TAB_PATH const isLiveNotesTabPath = (path: string) => path === LIVE_NOTES_TAB_PATH const isBgTasksTabPath = (path: string) => path === BG_TASKS_TAB_PATH const isEmailTabPath = (path: string) => path === EMAIL_TAB_PATH -const isWorkspaceTabPath = (path: string) => path === WORKSPACE_TAB_PATH -const isKnowledgeViewTabPath = (path: string) => path === KNOWLEDGE_VIEW_TAB_PATH -const isChatHistoryTabPath = (path: string) => path === CHAT_HISTORY_TAB_PATH -const isHomeTabPath = (path: string) => path === HOME_TAB_PATH const isBaseFilePath = (path: string) => path.endsWith('.base') || path === BASES_DEFAULT_TAB_PATH const getSuggestedTopicTargetFolder = (category?: string) => { @@ -580,17 +565,12 @@ type ViewState = | { type: 'meetings' } | { type: 'live-notes' } | { type: 'email' } - | { type: 'workspace'; path?: string } - | { type: 'knowledge-view' } - | { type: 'chat-history' } - | { type: 'home' } function viewStatesEqual(a: ViewState, b: ViewState): boolean { if (a.type !== b.type) return false if (a.type === 'chat' && b.type === 'chat') return a.runId === b.runId if (a.type === 'file' && b.type === 'file') return a.path === b.path if (a.type === 'task' && b.type === 'task') return a.name === b.name - if (a.type === 'workspace' && b.type === 'workspace') return (a.path ?? '') === (b.path ?? '') return true // both graph } @@ -634,16 +614,6 @@ function parseDeepLink(input: string): ViewState | null { return { type: 'meetings' } case 'live-notes': return { type: 'live-notes' } - case 'workspace': { - const path = params.get('path') - return { type: 'workspace', path: path ?? undefined } - } - case 'knowledge-view': - return { type: 'knowledge-view' } - case 'chat-history': - return { type: 'chat-history' } - case 'home': - return { type: 'home' } default: return null } @@ -657,7 +627,7 @@ function FixedSidebarToggle({ }) { const { toggleSidebar } = useSidebar() return ( -
+