Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
c37c5e2
feat: created auth client
DaniAkash Jan 22, 2026
8fcb8e9
feat: created login page for testing auth
DaniAkash Jan 22, 2026
f815109
feat: setup logout page
DaniAkash Jan 22, 2026
6fc0fdc
feat: setup graphql codegen
DaniAkash Jan 22, 2026
283a503
feat: setup graphql + react query utils
DaniAkash Jan 22, 2026
15bad5d
feat: setup queryprovider with localforage
DaniAkash Jan 22, 2026
75ff74a
feat: created auth provider
DaniAkash Jan 23, 2026
1f315f8
feat: update claude.md
DaniAkash Jan 23, 2026
84af25e
feat: documents for bulk conversation upload
DaniAkash Jan 23, 2026
aaf70a2
chore: install missing package
DaniAkash Jan 23, 2026
da3535d
fix: setup codegen to scan for .ts files
DaniAkash Jan 23, 2026
184c19a
chore: setup check conversation query
DaniAkash Jan 23, 2026
cc36c0f
chore: merge with main
DaniAkash Jan 23, 2026
20d5538
feat: upload conversation by profileId
DaniAkash Jan 26, 2026
214d6d5
chore: upload messages in batches
DaniAkash Jan 26, 2026
7c7b45c
feat: account for edge cases in conversation upload
DaniAkash Jan 26, 2026
8ef99df
feat: delete uploaded conversations from localstorage
DaniAkash Jan 26, 2026
b2d4f97
feat: load conversation history from api
DaniAkash Jan 26, 2026
cceaa02
feat: implement delete conversation using graphql
DaniAkash Jan 26, 2026
dbf4aa4
feat: delete confirmation for conversation history
DaniAkash Jan 26, 2026
f05e9c9
fix: issue with clearing conversations after upload
DaniAkash Jan 26, 2026
d711b61
feat: implement pagination for graphql chat history
DaniAkash Jan 26, 2026
aaf526d
chore: update CLAUDE.md
DaniAkash Jan 26, 2026
92341eb
chore: update claude.md
DaniAkash Jan 26, 2026
7ea1949
feat: save conversations to server
DaniAkash Jan 26, 2026
159b807
fix: handle streaming check on remote conversation save
DaniAkash Jan 26, 2026
d4d7186
feat: restore conversation from graphql
DaniAkash Jan 26, 2026
512921d
fix: timestamp issue on the chat history page
DaniAkash Jan 28, 2026
07e9c75
feat: sync llm providers from background script
DaniAkash Jan 28, 2026
5c81374
feat: update llm providers on change via background script
DaniAkash Jan 28, 2026
949ff36
chore: added a try catch block
DaniAkash Jan 28, 2026
f9fd942
feat: display incomplete providers in separate UI
DaniAkash Jan 28, 2026
acb03dd
feat: delete provider on server when initiated by user
DaniAkash Jan 28, 2026
fdf01fb
feat: setup scheduled tasks storage to sync to graphql
DaniAkash Jan 28, 2026
a32c281
feat: auto run sync in background script
DaniAkash Jan 28, 2026
482b6dc
fix: sync all keys of scheduled tasks based on updatedAt timestamp
DaniAkash Jan 28, 2026
6564acc
feat: added login dropdown on the sidebar
DaniAkash Jan 28, 2026
086e981
feat: simplify sidenav header
DaniAkash Jan 28, 2026
df17473
feat: update header design after login
DaniAkash Jan 28, 2026
f5400e0
feat: setup profile page
DaniAkash Jan 28, 2026
4df60e0
feat: added back button to profile page
DaniAkash Jan 28, 2026
af66ca3
fix: scrollbar flash in profile page
DaniAkash Jan 28, 2026
7e66437
feat: finish login handshake
DaniAkash Jan 29, 2026
9efb80d
feat: clear storage on logout
DaniAkash Jan 30, 2026
f0b392b
fix: logout page style
DaniAkash Jan 30, 2026
2981daf
feat: added tooltip to encourage user to sign in
DaniAkash Jan 30, 2026
ce43dc8
feat: added back button to login page
DaniAkash Jan 30, 2026
7d9c1fd
fix: upload logic for profile picture
DaniAkash Jan 30, 2026
09dd794
feat: account for profile name in sidebar branding
DaniAkash Jan 30, 2026
126e2fd
chore: set file upload url from backend request
DaniAkash Jan 30, 2026
3c1d09c
chore: remove default placeholder from profile component
DaniAkash Jan 30, 2026
77e06b8
chore: sync with main
DaniAkash Jan 30, 2026
dd921d9
Revert "chore: sync with main"
DaniAkash Jan 30, 2026
e10c4a3
Reapply "chore: sync with main"
DaniAkash Jan 30, 2026
6f84cff
chore: updated lock file
DaniAkash Jan 30, 2026
243e666
fix: run codegen before build:ext
shadowfax92 Jan 30, 2026
9d1d030
fix: run codegen before build:gent
shadowfax92 Jan 30, 2026
6721d10
chore: sync with main
DaniAkash Jan 30, 2026
36530f1
chore: sync with main
DaniAkash Jan 30, 2026
2b4c511
fix: remove hardcoded localhost header in magic link
DaniAkash Jan 30, 2026
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
6 changes: 6 additions & 0 deletions apps/agent/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ VITE_PUBLIC_POSTHOG_KEY=
VITE_PUBLIC_POSTHOG_HOST=
VITE_PUBLIC_SENTRY_DSN=

# BrowserOS API URL
VITE_PUBLIC_BROWSEROS_API=

# GraphQL Schema Path
GRAPHQL_SCHEMA_PATH=

# Sentry build (source maps)
SENTRY_AUTH_TOKEN=
SENTRY_ORG=
Expand Down
3 changes: 3 additions & 0 deletions apps/agent/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ stats-*.json
# Env files
.env*
!.env.example

# GraphQL generated files
generated/
102 changes: 102 additions & 0 deletions apps/agent/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,105 @@ The key directories of the project are:
- `entrypoints/newtab`: Contains the code for the new tab page of the extension.
- `entrypoints/popup`: Contains the code for the popup that appears when the extension icon is clicked.
- `entrypoints/onboarding`: Contains the onboarding flow for new users which is triggered on first install.

## React Coding patterns

- Avoid using useCallback and useMemo as much as possible - only add them if their presence is absolutely necessary
- When writing a graphql document, create a /graphql directory under the current directory where the file is present and create a file to contain the document.
- For example: if you want to create grapqhl queries in @apps/agent/entrypoints/sidepanel/history/ChatHistory.tsx then write the graphql document in @apps/agent/entrypoints/sidepanel/history/graphql/chatHistoryDocument.ts
- Shadcn UI is setup in this project and always use shadcn components for the UI
- When need to record errors, do not use console.error -> instead use the sentry service to capture errors:
```ts
import { sentry } from '@/lib/sentry/sentry'

sentry.captureException(error, {
extra: {
message: 'Failed to fetch graph data from the server',
codeId: workflow.codeId,
},
})
```

## GraphQL Client

- The Graphql main schema file is in `@apps/agent/generated/graphql/schema.graphql` - this is the source of truth for constructing all graphql queries

- The frontend uses React Query with `graphql-codegen` to interact with the backend GraphQL API. The types are generated and stored in `@apps/agent/generated/graphql`

- When working with React Query and GraphQL, some important utilities are already created to make the interaction simpler:
- `@apps/agent/lib/graphql/useGraphqlInfiniteQuery.ts`
- `@apps/agent/lib/graphql/useGraphqlMutation.ts`
- `@apps/agent/lib/graphql/useGraphqlQuery.ts`
- `@apps/agent/lib/graphql/getQueryKeyFromDocument.ts`

This is how a standard GraphQL query and mutation looks like:

```ts
import { graphql } from "~/graphql/gql";
import { useGraphqlQuery } from "@/lib/graphql/useGraphqlQuery";
import { useGraphqlMutation } from "@/lib/graphql/useGraphqlMutation";
import { useSessionInfo } from '@/lib/auth/sessionStorage'
import { getQueryKeyFromDocument } from "@/modules/graphql/getQueryKeyFromDocument";
import { useQueryClient } from "@tanstack/react-query";

export const GetProfileByUserIdDocument = graphql(`
query GetProfileByUserId($userId: String!) {
profileByUserId(userId: $userId) {
id
rowId
name
userId
meta
profilePictureUrl
linkedInUrl
updatedAt
createdAt
deletedAt
}
}
`);

const UpdateProfileIndustryDocument = graphql(`
mutation UpdateProfileIndustry($userId: String!, $meta: JSON) {
updateProfileByUserId(input: { userId: $userId, patch: { meta: $meta } }) {
profile {
id
rowId
meta
}
}
}
`);

const { sessionInfo } = useSessionInfo()

const userId = sessionInfo.user?.id

const queryClient = useQueryClient();

const { data: profileData } = useGraphqlQuery(
GetProfileByUserIdDocument,
{
userId,
},
{
enabled: !!userId,
},
);

const updateProfileMutation = useGraphqlMutation(
UpdateProfileIndustryDocument,
{
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: [getQueryKeyFromDocument(GetProfileByUserIdDocument)],
});
},
},
);
```

To run codegen to generate graphql code after creating a query, you should run codegen using the command (since .env.development is necessary for codegen):
```sh
bun --env-file=.env.development run codegen
```
39 changes: 39 additions & 0 deletions apps/agent/codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import path from 'node:path'
import { includeIgnoreFile } from '@eslint/compat'
import type { CodegenConfig } from '@graphql-codegen/cli'

// biome-ignore lint/style/noProcessEnv: env needed for codegen config
const env = process.env

const schemaPath = env.GRAPHQL_SCHEMA_PATH as string

const gitignorePath = path.resolve(__dirname, '.gitignore')

const ignorePatterns = includeIgnoreFile(
gitignorePath,
'Imported .gitignore patterns',
)

const ignoresList = ignorePatterns.ignores?.map((each) => `!${each}`) ?? []

const config: CodegenConfig = {
schema: schemaPath,
documents: ['./**/*.tsx', './**/*.ts', ...ignoresList],
ignoreNoDocuments: true,
generates: {
'./generated/graphql/': {
preset: 'client',
config: {
documentMode: 'string',
},
},
'./generated/graphql/schema.graphql': {
plugins: ['schema-ast'],
config: {
includeDirectives: true,
},
},
},
}

export default config
27 changes: 27 additions & 0 deletions apps/agent/components/auth/AuthGuard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Loader2 } from 'lucide-react'
import type { FC, ReactNode } from 'react'
import { Navigate, useLocation } from 'react-router'
import { useSession } from '@/lib/auth/auth-client'

interface AuthGuardProps {
children: ReactNode
}

export const AuthGuard: FC<AuthGuardProps> = ({ children }) => {
const { data: session, isPending } = useSession()
const location = useLocation()

if (isPending) {
return (
<div className="flex min-h-screen items-center justify-center bg-background">
<Loader2 className="size-8 animate-spin text-muted-foreground" />
</div>
)
}

if (!session) {
return <Navigate to="/login" state={{ from: location }} replace />
}

return <>{children}</>
}
151 changes: 138 additions & 13 deletions apps/agent/components/sidebar/SidebarBranding.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
import { ChevronDown, LogIn, LogOut, User } from 'lucide-react'
import type { FC } from 'react'
import { useNavigate } from 'react-router'
import ProductLogo from '@/assets/product_logo.svg'
import { ThemeToggle } from '@/components/elements/theme-toggle'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { GetProfileByUserIdDocument } from '@/entrypoints/app/profile/graphql/profileDocument'
import { useSessionInfo } from '@/lib/auth/sessionStorage'
import { useGraphqlQuery } from '@/lib/graphql/useGraphqlQuery'
import { cn } from '@/lib/utils'
import { useWorkspace } from '@/lib/workspace/use-workspace'

Expand All @@ -12,25 +25,137 @@ export const SidebarBranding: FC<SidebarBrandingProps> = ({
expanded = true,
}) => {
const { selectedFolder } = useWorkspace()
const { sessionInfo } = useSessionInfo()
const navigate = useNavigate()

return (
<div className="flex h-14 items-center border-b">
<div className="flex w-14 shrink-0 items-center justify-center">
<img src={ProductLogo} alt="BrowserOS" className="size-8" />
const user = sessionInfo?.user
const isLoggedIn = !!user

const { data: profileData } = useGraphqlQuery(
GetProfileByUserIdDocument,
{ userId: user?.id ?? '' },
{ enabled: !!user?.id },
)

const profile = profileData?.profileByUserId
const profileName =
profile?.firstName || profile?.lastName
? [profile.firstName, profile.lastName].filter(Boolean).join(' ')
: null
const displayName = profileName || user?.name || 'User'
const displayImage = profile?.avatarUrl || user?.image

const getInitials = (name?: string | null) => {
if (!name) return '?'
return name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2)
}

const headerIcon = isLoggedIn ? (
displayImage ? (
<img
src={displayImage}
alt={displayName}
className="size-8 shrink-0 rounded-full object-cover"
/>
) : (
<div className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary font-medium text-primary-foreground text-xs">
{getInitials(displayName)}
</div>
)
) : (
<img src={ProductLogo} alt="BrowserOS" className="size-8" />
)

return (
<div className="flex h-14 items-center justify-between border-b px-2">
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn(
'flex cursor-pointer items-center gap-2 rounded-lg p-1.5 text-left transition-colors hover:bg-sidebar-accent focus-visible:outline-none',
expanded ? 'pr-3' : '',
)}
>
{headerIcon}
<div
className={cn(
'flex min-w-0 flex-col gap-0.5 leading-none transition-opacity duration-200',
expanded ? 'opacity-100' : 'hidden',
)}
>
<div className="flex items-center gap-1">
<span className="truncate font-semibold">
{isLoggedIn
? displayName
: selectedFolder?.name || 'BrowserOS'}
</span>
<ChevronDown className="size-3.5 shrink-0 text-muted-foreground" />
</div>
<span
className={cn(
'truncate text-xs',
isLoggedIn
? 'text-muted-foreground'
: 'font-medium text-primary',
)}
>
{isLoggedIn ? 'Personal' : 'Sign in'}
</span>
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent
side={expanded ? 'bottom' : 'right'}
align="start"
className="w-56"
>
{isLoggedIn ? (
<>
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="font-medium text-sm leading-none">
{displayName}
</p>
<p className="text-muted-foreground text-xs leading-none">
Personal
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => navigate('/profile')}>
<User className="mr-2 size-4" />
Update Profile
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => navigate('/logout')}
variant="destructive"
>
<LogOut className="mr-2 size-4" />
Sign out
</DropdownMenuItem>
</>
) : (
<DropdownMenuItem onClick={() => navigate('/login')}>
<LogIn className="mr-2 size-4" />
Sign in
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<div
className={cn(
'flex min-w-0 flex-1 items-center justify-between pr-3 transition-opacity duration-200',
expanded ? 'opacity-100' : 'opacity-0',
'shrink-0 transition-opacity duration-200',
expanded ? 'opacity-100' : 'hidden',
)}
>
<div className="flex min-w-0 flex-col gap-0.5 leading-none">
<span className="truncate font-semibold">
{selectedFolder?.name || 'BrowserOS'}
</span>
<span className="text-muted-foreground text-xs">Personal</span>
</div>
<ThemeToggle className="h-8 w-8 shrink-0" iconClassName="h-4 w-4" />
<ThemeToggle className="h-8 w-8" iconClassName="h-4 w-4" />
</div>
</div>
)
Expand Down
Loading
Loading