Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ dist/
Thumbs.db

# playright
.playwright-mcp/
browser-extension/dist-playground/
browser-extension/playwright-report/
browser-extension/playwright/
browser-extension/test-results/
browser-extension/test-results/
22 changes: 10 additions & 12 deletions browser-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ This is a [WXT](https://wxt.dev/)-based browser extension that

### Entry points

- `src/entrypoints/content.ts` - injected into every webpage
- `src/entrypoints/background.ts` - service worker that manages state and handles messages
- `src/entrypoints/popup` - html/css/ts which opens when the extension's button gets clicked
- [`src/entrypoints/content.ts`](src/entrypoints/content.ts) - injected into every webpage
- [`src/entrypoints/background.ts`](src/entrypoints/background.ts) - service worker that manages state and handles messages
- [`src/entrypoints/popup/popup.tsx`](src/entrypoints/popup/popup.tsx) - popup (html/css/tsx) with shadcn/ui table components

```mermaid
graph TD
Content[Content Script<br/>content.ts]
Background[Background Script<br/>background.ts]
Popup[Popup Script<br/>popup/main.ts]
Popup[Popup Script<br/>popup/popup.tsx]

Content -->|ENHANCED/DESTROYED<br/>CommentEvent| Background
Popup -->|GET_OPEN_SPOTS<br/>SWITCH_TO_TAB| Background
Expand All @@ -60,22 +60,20 @@ graph TD
class TextArea,UI ui
```

### Architecture
Every time a `textarea` shows up on a page, on initial load or later on, it gets passed to a list of `CommentEnhancer`s. Each one gets a turn to say "I can enhance this box!". They show that they can enhance it by returning something non-null in the method `tryToEnhance(textarea: HTMLTextAreaElement): Spot | null`. Later on, that same `Spot` data will be used by the `tableRow(spot: Spot): ReactNode` method to create React components for rich formatting in the popup table.

Every time a `textarea` shows up on a page, on initial load or later on, it gets passed to a list of `CommentEnhancer`s. Each one gets a turn to say "I can enhance this box!". They show that they can enhance it by returning a [`CommentSpot`, `Overtype`].
Those `Spot` values get bundled up with the `HTMLTextAreaElement` itself into an `EnhancedTextarea`, which gets added to the `TextareaRegistry`. At some interval, draft edits get saved by the browser extension.

Those values get bundled up with the `HTMLTextAreaElement` itself into an `EnhancedTextarea`, which gets added to the `TextareaRegistry`. At some interval, draft edits will get saved by the browser extension (TODO).

When the `textarea` gets removed from the page, the `TextareaRegistry` is notified so that the `CommentSpot` can be marked as abandoned or submitted as appropriate (TODO).
When the `textarea` gets removed from the page, the `TextareaRegistry` is notified so that the `CommentSpot` can be marked as abandoned or submitted as appropriate.

## Testing

In `tests/har` there are various `.har` files. These are complete recordings of a single page load.

- `pnpm run har:view` and you can see the recordings, with or without our browser extension.
- `npm run playground` gives you a test environment where you can tinker with the popup with various test data, supports hot reload
- `npm run har:view` gives you recordings of various web pages which you can see with and without enhancement by the browser extension

### Recording new HAR files

- the har recordings live in `tests/har`, they are complete recordings of the network requests of a single page load
- you can add or change URLs in `tests/har-index.ts`
- `npx playwright codegen https://github.com/login --save-storage=playwright/.auth/gh.json` will store new auth tokens
- login manually, then close the browser
Expand Down
1 change: 1 addition & 0 deletions browser-extension/biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
}
},
"noExplicitAny": "off",
"noUnknownAtRules": "off",
"noVar": "error"
}
}
Expand Down
21 changes: 21 additions & 0 deletions browser-extension/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/styles/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}
16 changes: 16 additions & 0 deletions browser-extension/package.json
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
{
"author": "DiffPlug",
"dependencies": {
"@primer/octicons-react": "^19.18.0",
"@radix-ui/react-slot": "^1.2.3",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"@wxt-dev/webextension-polyfill": "^1.0.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"highlight.js": "^11.11.1",
"lucide-react": "^0.543.0",
"overtype": "workspace:*",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"tailwind-merge": "^3.3.1",
"webextension-polyfill": "^0.12.0"
},
"description": "Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).",
"devDependencies": {
"@biomejs/biome": "^2.1.2",
"@playwright/test": "^1.46.0",
"@tailwindcss/vite": "^4.1.13",
"@testing-library/jest-dom": "^6.6.4",
"@types/express": "^4.17.21",
"@types/har-format": "^1.2.16",
"@types/node": "^22.16.5",
"@vitejs/plugin-react": "^5.0.2",
"@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"express": "^4.19.2",
"linkedom": "^0.18.12",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.13",
"tsx": "^4.19.1",
"typescript": "^5.8.3",
"vite": "^7.1.5",
"vitest": "^3.2.4",
"wxt": "^0.20.7"
},
Expand Down Expand Up @@ -47,6 +62,7 @@
"dev:firefox": "wxt -b firefox",
"postinstall": "wxt prepare",
"test": "vitest run",
"playground": "vite --config vite.playground.config.ts",
"har:record": "tsx tests/har-record.ts",
"har:view": "tsx tests/har-view.ts"
},
Expand Down
40 changes: 40 additions & 0 deletions browser-extension/src/components/SpotRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { TableCell, TableRow } from '@/components/ui/table'
import type { CommentState } from '@/entrypoints/background'
import type { EnhancerRegistry } from '@/lib/registries'
import { cn } from '@/lib/utils'

interface SpotRowProps {
commentState: CommentState
enhancerRegistry: EnhancerRegistry
onClick: () => void
className?: string
cellClassName?: string
errorClassName?: string
}

export function SpotRow({
commentState,
enhancerRegistry,
onClick,
className,
cellClassName = 'p-3',
errorClassName = 'text-red-500',
}: SpotRowProps) {
const enhancer = enhancerRegistry.enhancerFor(commentState.spot)

if (!enhancer) {
return (
<TableRow className={cn('cursor-pointer', className)} onClick={onClick}>
<TableCell className={cellClassName}>
<div className={errorClassName}>Unknown spot type: {commentState.spot.type}</div>
</TableCell>
</TableRow>
)
}

return (
<TableRow className={cn('cursor-pointer', className)} onClick={onClick}>
<TableCell className={cellClassName}>{enhancer.tableRow(commentState.spot)}</TableCell>
</TableRow>
)
}
78 changes: 78 additions & 0 deletions browser-extension/src/components/SpotTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Table, TableBody, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import type { CommentState } from '@/entrypoints/background'
import type { EnhancerRegistry } from '@/lib/registries'
import { SpotRow } from './SpotRow'

interface SpotTableProps {
spots: CommentState[]
enhancerRegistry: EnhancerRegistry
onSpotClick: (spot: CommentState) => void
title?: string
description?: string
headerText?: string
className?: string
headerClassName?: string
rowClassName?: string
cellClassName?: string
emptyStateMessage?: string
showHeader?: boolean
}

export function SpotTable({
spots,
enhancerRegistry,
onSpotClick,
title,
description,
headerText = 'Comment Spots',
className,
headerClassName = 'p-3 font-medium text-muted-foreground',
rowClassName,
cellClassName,
emptyStateMessage = 'No comment spots available',
showHeader = true,
}: SpotTableProps) {
if (spots.length === 0) {
return <div className='p-10 text-center text-muted-foreground italic'>{emptyStateMessage}</div>
}

const tableContent = (
<Table>
{showHeader && (
<TableHeader>
<TableRow>
<TableHead className={headerClassName}>{headerText}</TableHead>
</TableRow>
</TableHeader>
)}
<TableBody>
{spots.map((spot) => (
<SpotRow
key={spot.spot.unique_key}
commentState={spot}
enhancerRegistry={enhancerRegistry}
onClick={() => onSpotClick(spot)}
className={rowClassName || ''}
cellClassName={cellClassName || 'p-3'}
/>
))}
</TableBody>
</Table>
)

if (title || description) {
return (
<div className={className}>
{(title || description) && (
<div className='p-6 border-b border-border'>
{title && <h2 className='text-xl font-semibold text-foreground'>{title}</h2>}
{description && <p className='text-muted-foreground text-sm mt-1'>{description}</p>}
</div>
)}
{tableContent}
</div>
)
}

return <div className={className}>{tableContent}</div>
}
49 changes: 49 additions & 0 deletions browser-extension/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import * as React from 'react'

import { cn } from '@/lib/utils'

const buttonVariants = cva(
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
{
defaultVariants: {
size: 'default',
variant: 'default',
},
variants: {
size: {
default: 'h-10 px-4 py-2',
icon: 'h-10 w-10',
lg: 'h-11 rounded-md px-8',
sm: 'h-9 rounded-md px-3',
},
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
},
},
},
)

export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}

const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp className={cn(buttonVariants({ className, size, variant }))} ref={ref} {...props} />
)
},
)
Button.displayName = 'Button'

export { Button, buttonVariants }
Loading