Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions apps/agent/components/sidebar/SettingsSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import { NavLink, useLocation } from 'react-router'
import { ThemeToggle } from '@/components/elements/theme-toggle'
import { Feature } from '@/lib/browseros/capabilities'
import { useCapabilities } from '@/lib/browseros/useCapabilities'
import {
getOnboardingFeaturesPath,
getOnboardingRevisitPath,
} from '@/lib/onboarding/onboardingFlow'
import { cn } from '@/lib/utils'

type NavItem = {
Expand Down Expand Up @@ -43,8 +47,16 @@ const settingsNavItems: NavItem[] = [
feature: Feature.SOUL_SUPPORT,
},
{ name: 'Skills', to: '/settings/skills', icon: Wand2 },
{ name: 'Explore Features', to: '/onboarding/features', icon: Compass },
{ name: 'Revisit Onboarding', to: '/onboarding', icon: RotateCcw },
{
name: 'Explore Features',
to: getOnboardingFeaturesPath('settings'),
icon: Compass,
},
{
name: 'Revisit Onboarding',
to: getOnboardingRevisitPath(),
icon: RotateCcw,
},
]

export const SettingsSidebar: FC = () => {
Expand Down Expand Up @@ -78,7 +90,7 @@ export const SettingsSidebar: FC = () => {
<nav className="space-y-1">
{filteredItems.map((item) => {
const Icon = item.icon
const isActive = location.pathname === item.to
const isActive = location.pathname === item.to.split('?')[0]

return (
<NavLink
Expand Down
6 changes: 4 additions & 2 deletions apps/agent/entrypoints/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { HashRouter, Navigate, Route, Routes, useParams } from 'react-router'
import { NewTab } from '../newtab/index/NewTab'
import { NewTabLayout } from '../newtab/layout/NewTabLayout'
import { Personalize } from '../newtab/personalize/Personalize'
import { OnboardingDemo } from '../onboarding/demo/OnboardingDemo'
import { FeaturesPage } from '../onboarding/features/Features'
import { Onboarding } from '../onboarding/index/Onboarding'
import { StepsLayout } from '../onboarding/steps/StepsLayout'
Expand Down Expand Up @@ -108,7 +107,10 @@ export const App: FC = () => {
<Route path="onboarding">
<Route index element={<Onboarding />} />
<Route path="steps/:stepId" element={<StepsLayout />} />
<Route path="demo" element={<OnboardingDemo />} />
<Route
path="demo"
element={<Navigate to="/onboarding/steps/5" replace />}
/>
<Route path="features" element={<FeaturesPage />} />
</Route>

Expand Down
3 changes: 3 additions & 0 deletions apps/agent/entrypoints/app/login/LoginPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import { signIn, useSession } from '@/lib/auth/auth-client'
import { authRedirectPathStorage } from '@/lib/onboarding/onboardingStorage'

type LoginState = 'idle' | 'loading' | 'magic-link-sent' | 'error'

Expand All @@ -45,6 +46,7 @@ export const LoginPage: FC = () => {
setError(null)

try {
await authRedirectPathStorage.removeValue()
const result = await signIn.magicLink({
email: email.trim(),
callbackURL: '/home',
Expand All @@ -68,6 +70,7 @@ export const LoginPage: FC = () => {
setError(null)

try {
await authRedirectPathStorage.removeValue()
await signIn.social({
provider: 'google',
callbackURL: '/home',
Expand Down
18 changes: 16 additions & 2 deletions apps/agent/entrypoints/app/login/MagicLinkCallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
CardTitle,
} from '@/components/ui/card'
import { useSession } from '@/lib/auth/auth-client'
import { authRedirectPathStorage } from '@/lib/onboarding/onboardingStorage'

export const MagicLinkCallback: FC = () => {
const navigate = useNavigate()
Expand All @@ -20,14 +21,27 @@ export const MagicLinkCallback: FC = () => {
const [error, setError] = useState<string | null>(null)

useEffect(() => {
let cancelled = false
const errorParam = searchParams.get('error')
if (errorParam) {
setError(decodeURIComponent(errorParam))
return
}

if (!isPending && session) {
navigate('/home', { replace: true })
if (isPending || !session) return

const redirectAfterAuth = async () => {
const redirectPath = await authRedirectPathStorage.getValue()
if (redirectPath) {
await authRedirectPathStorage.removeValue()
}
if (cancelled) return
navigate(redirectPath || '/home', { replace: true })
}

void redirectAfterAuth()
return () => {
cancelled = true
}
}, [session, isPending, searchParams, navigate])

Expand Down
70 changes: 70 additions & 0 deletions apps/agent/entrypoints/newtab/index/NewTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ import { useMcpServers } from '@/lib/mcp/mcpServerStorage'
import { useSyncRemoteIntegrations } from '@/lib/mcp/useSyncRemoteIntegrations'
import { openSidePanelWithSearch } from '@/lib/messaging/sidepanel/openSidepanelWithSearch'
import { track } from '@/lib/metrics/track'
import {
getSearchActionFingerprint,
isSearchActionForTarget,
searchActionsStorage,
} from '@/lib/search-actions/searchActionsStorage'
import { cn } from '@/lib/utils'
import { useWorkspace } from '@/lib/workspace/use-workspace'
import { ImportDataHint } from './ImportDataHint'
Expand Down Expand Up @@ -98,6 +103,16 @@ export const NewTab = () => {

const { messages, sendMessage, setMode, resetConversation } =
useChatSessionContext()
const sendMessageRef = useRef(sendMessage)
const setModeRef = useRef(setMode)
const resetConversationRef = useRef(resetConversation)
const processingSearchActionRef = useRef<string | null>(null)

useEffect(() => {
sendMessageRef.current = sendMessage
setModeRef.current = setMode
resetConversationRef.current = resetConversation
}, [resetConversation, sendMessage, setMode])

const connectedManagedServers = mcpServers.filter((s) => {
if (s.type !== 'managed' || !s.managedServerName) return false
Expand Down Expand Up @@ -357,6 +372,61 @@ export const NewTab = () => {
setChatActive(false)
}

useEffect(() => {
let cancelled = false

const consumeSearchAction = async (
storageAction: Awaited<ReturnType<typeof searchActionsStorage.getValue>>,
) => {
const currentTab = await chrome.tabs.getCurrent().catch(() => undefined)
const currentTabId = currentTab?.id

if (
cancelled ||
!storageAction ||
!isSearchActionForTarget(storageAction, 'newtab', currentTabId)
) {
return
}

const fingerprint = getSearchActionFingerprint(storageAction)
if (processingSearchActionRef.current === fingerprint) {
return
}

processingSearchActionRef.current = fingerprint

try {
await searchActionsStorage.removeValue()
if (cancelled) return

resetConversationRef.current()
setModeRef.current(storageAction.mode)
setChatActive(true)
sendMessageRef.current({
text: storageAction.query,
action: storageAction.action,
})
} finally {
processingSearchActionRef.current = null
}
}

searchActionsStorage
.getValue()
.then(consumeSearchAction)
.catch(() => {})

const unwatch = searchActionsStorage.watch((storageAction) => {
consumeSearchAction(storageAction).catch(() => {})
})

return () => {
cancelled = true
unwatch()
}
}, [])

const isSuggestionsVisible =
!mentionState.isOpen &&
((isOpen && inputValue.length) ||
Expand Down
166 changes: 4 additions & 162 deletions apps/agent/entrypoints/onboarding/demo/OnboardingDemo.tsx
Original file line number Diff line number Diff line change
@@ -1,164 +1,6 @@
import { ArrowRight, Sparkles } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import {
ONBOARDING_COMPLETED_EVENT,
ONBOARDING_DEMO_TRIGGERED_EVENT,
} from '@/lib/constants/analyticsEvents'
import { openSidePanelWithSearch } from '@/lib/messaging/sidepanel/openSidepanelWithSearch'
import { track } from '@/lib/metrics/track'
import {
onboardingCompletedStorage,
onboardingProfileStorage,
} from '@/lib/onboarding/onboardingStorage'
import type { FC } from 'react'
import { Navigate } from 'react-router'

function buildDemoSuggestions(company?: string) {
return [
company
? {
label: `Search for ${company} and summarize the latest news`,
query: `Search for ${company} and summarize the latest news about them`,
mode: 'agent' as const,
}
: {
label: "What's the top tech news today",
query: "What's the top tech news today? Give me a brief summary",
mode: 'agent' as const,
},
{
label: "What's the top news today",
query:
"What's the top news today? Give me a brief summary of the biggest stories",
mode: 'agent' as const,
},
{
label: 'Find me a good restaurant nearby',
query: 'Find me a good restaurant nearby',
mode: 'agent' as const,
},
]
}

export const OnboardingDemo = () => {
const [customQuery, setCustomQuery] = useState('')
const [demoSuggestions, setDemoSuggestions] = useState(() =>
buildDemoSuggestions(),
)

useEffect(() => {
onboardingProfileStorage.getValue().then((profile) => {
if (profile?.company) {
setDemoSuggestions(buildDemoSuggestions(profile.company))
}
})
}, [])

const completeOnboarding = async () => {
await onboardingCompletedStorage.setValue(true)
track(ONBOARDING_COMPLETED_EVENT)
}

const handleDemoTask = async (
query: string,
mode: 'chat' | 'agent',
index: number,
) => {
track(ONBOARDING_DEMO_TRIGGERED_EVENT, {
query,
mode,
source: 'suggestion',
suggestion_index: index,
})
await completeOnboarding()

await chrome.tabs.create({ active: true })
await new Promise((resolve) => setTimeout(resolve, 500))
openSidePanelWithSearch('open', { query, mode })
}

const handleCustomQuery = async (e: React.FormEvent) => {
e.preventDefault()
if (!customQuery.trim()) return

track(ONBOARDING_DEMO_TRIGGERED_EVENT, {
query: customQuery.trim(),
mode: 'agent',
source: 'custom',
})
await completeOnboarding()

await chrome.tabs.create({ active: true })
await new Promise((resolve) => setTimeout(resolve, 500))
openSidePanelWithSearch('open', {
query: customQuery.trim(),
mode: 'agent',
})
}

const handleSkip = async () => {
track(ONBOARDING_DEMO_TRIGGERED_EVENT, { skipped: true })
await completeOnboarding()
window.location.href = chrome.runtime.getURL('app.html#/home')
}

return (
<div className="flex h-screen flex-col items-center justify-center bg-background px-6">
<div className="w-full max-w-lg space-y-8">
<div className="space-y-2 text-center">
<div className="mx-auto mb-4 flex size-12 items-center justify-center rounded-full bg-[var(--accent-orange)]/10">
<Sparkles className="size-6 text-[var(--accent-orange)]" />
</div>
<h2 className="font-bold text-3xl tracking-tight">
Try your first task
</h2>
<p className="text-base text-muted-foreground">
Pick a suggestion or type your own to see BrowserOS in action
</p>
</div>

<div className="space-y-3">
{demoSuggestions.map((suggestion, index) => (
<button
key={suggestion.label}
type="button"
onClick={() =>
handleDemoTask(suggestion.query, suggestion.mode, index)
}
className="flex w-full items-center justify-between rounded-lg border border-border bg-card p-4 text-left transition-colors hover:border-[var(--accent-orange)]/50 hover:bg-accent"
>
<span className="font-medium text-sm">{suggestion.label}</span>
<ArrowRight className="size-4 text-muted-foreground" />
</button>
))}
</div>

<form onSubmit={handleCustomQuery} className="flex gap-2">
<Input
placeholder="Or type your own task..."
value={customQuery}
onChange={(e) => setCustomQuery(e.target.value)}
className="flex-1"
/>
<Button
type="submit"
disabled={!customQuery.trim()}
className="bg-[var(--accent-orange)] text-white hover:bg-[var(--accent-orange)]/90"
>
Go
</Button>
</form>

<div className="text-center">
<Button
variant="ghost"
onClick={handleSkip}
className="text-muted-foreground"
>
Skip and go to homepage
</Button>
</div>
</div>
</div>
)
export const OnboardingDemo: FC = () => {
return <Navigate to="/onboarding/steps/5" replace />
}
Loading
Loading