Skip to content
Merged
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
f5e5b37
Maintain a map to retrieve an enhancer for a commentspot.
nedtwigg Sep 9, 2025
cbb537b
`JsonMap` which can use json as a map key.
nedtwigg Sep 9, 2025
b825274
Add new data types needed for the next step.
nedtwigg Sep 9, 2025
165aaf9
Merge branch 'main' into feat/popup-poc
nedtwigg Sep 9, 2025
ded9837
Slim down JsonMap.
nedtwigg Sep 9, 2025
d0f705d
First cut.
nedtwigg Sep 9, 2025
3ed7d5f
Remove greebles.
nedtwigg Sep 9, 2025
6d7cd11
use the proper WXT function name.
nedtwigg Sep 9, 2025
3744358
Rudimentary integration test between the tabs and hub.
nedtwigg Sep 9, 2025
b083a7b
better use of Typescript's type system
nedtwigg Sep 9, 2025
97da132
tighten up the background test.
nedtwigg Sep 9, 2025
d482512
Strip lorum ipsum out of the popup.
nedtwigg Sep 9, 2025
8befd38
Rename `states` to `openSpots`.
nedtwigg Sep 9, 2025
5f0eac8
first cut at popup implementation
nedtwigg Sep 9, 2025
7627253
Add a bit of logging.
nedtwigg Sep 9, 2025
25b46a7
This has plenty of problems, but it does turn on!
nedtwigg Sep 9, 2025
f3734c6
working at a basic level
nedtwigg Sep 9, 2025
2334895
Leverage the typesystem.
nedtwigg Sep 9, 2025
8bdf3b8
Add types for the flag which determines if a response is going to be …
nedtwigg Sep 9, 2025
15adc2e
Add a handy precommit script to `browser-extension`.
nedtwigg Sep 10, 2025
b020899
Oops, forgot a sort.
nedtwigg Sep 10, 2025
e8ca1d6
Add a mermaid diagram for the new layout.
nedtwigg Sep 10, 2025
a8fffaa
We can simplify by keying on `unique_key` like we're supposed to, lol
nedtwigg Sep 10, 2025
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
24 changes: 24 additions & 0 deletions browser-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,32 @@ 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

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

Content -->|ENHANCED/DESTROYED<br/>CommentEvent| Background
Popup -->|GET_OPEN_SPOTS<br/>SWITCH_TO_TAB| Background
Background -->|GetOpenSpotsResponse<br/>spots array| Popup

Background -.->|manages| Storage[Comment State Storage<br/>openSpots JsonMap]
Content -.->|enhances| TextArea[textarea elements<br/>on web pages]
Popup -.->|displays| UI[Extension UI<br/>list of comment spots]

classDef entrypoint fill:#e1f5fe
classDef storage fill:#f3e5f5
classDef ui fill:#e8f5e8

class Content,Background,Popup entrypoint
class Storage storage
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 a [`CommentSpot`, `Overtype`].
Expand Down
1 change: 1 addition & 0 deletions browser-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"build": "wxt build",
"build:dev": "wxt build --mode development",
"build:firefox": "wxt build -b firefox",
"precommit": "npm run biome:fix && npm run typecheck && npm run test",
"typecheck": "tsc --noEmit",
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
Expand Down
85 changes: 85 additions & 0 deletions browser-extension/src/entrypoints/background.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { CommentDraft, CommentEvent, CommentSpot } from '../lib/enhancer'
import type { GetOpenSpotsResponse, ToBackgroundMessage } from '../lib/messages'
import {
CLOSE_MESSAGE_PORT,
isContentToBackgroundMessage,
isGetOpenSpotsMessage,
isSwitchToTabMessage,
KEEP_PORT_OPEN,
} from '../lib/messages'

export interface Tab {
tabId: number
windowId: number
}
export interface CommentState {
tab: Tab
spot: CommentSpot
drafts: [number, CommentDraft][]
}

export const openSpots = new Map<string, CommentState>()

export function handleCommentEvent(message: CommentEvent, sender: any): boolean {
if (
(message.type === 'ENHANCED' || message.type === 'DESTROYED') &&
sender.tab?.id &&
sender.tab?.windowId
) {
if (message.type === 'ENHANCED') {
const tab: Tab = {
tabId: sender.tab.id,
windowId: sender.tab.windowId,
}
const commentState: CommentState = {
drafts: [],
spot: message.spot,
tab,
}
openSpots.set(message.spot.unique_key, commentState)
} else if (message.type === 'DESTROYED') {
openSpots.delete(message.spot.unique_key)
} else {
throw new Error(`Unhandled comment event type: ${message.type}`)
}
}
return CLOSE_MESSAGE_PORT
}

export function handlePopupMessage(
message: any,
_sender: any,
sendResponse: (response: any) => void,
): typeof CLOSE_MESSAGE_PORT | typeof KEEP_PORT_OPEN {
if (isGetOpenSpotsMessage(message)) {
const spots: CommentState[] = []
for (const [, commentState] of openSpots) {
spots.push(commentState)
}
const response: GetOpenSpotsResponse = { spots }
sendResponse(response)
return KEEP_PORT_OPEN
} else if (isSwitchToTabMessage(message)) {
browser.windows
.update(message.windowId, { focused: true })
.then(() => {
return browser.tabs.update(message.tabId, { active: true })
})
.catch((error) => {
console.error('Error switching to tab:', error)
})
return CLOSE_MESSAGE_PORT
} else {
throw new Error(`Unhandled popup message type: ${message?.type || 'unknown'}`)
}
}

export default defineBackground(() => {
browser.runtime.onMessage.addListener((message: ToBackgroundMessage, sender, sendResponse) => {
if (isContentToBackgroundMessage(message)) {
return handleCommentEvent(message, sender)
} else {
return handlePopupMessage(message, sender, sendResponse)
}
})
})
16 changes: 16 additions & 0 deletions browser-extension/src/entrypoints/content.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import { CONFIG, type ModeType } from '../lib/config'
import type { CommentEvent, CommentSpot } from '../lib/enhancer'
import { logger } from '../lib/logger'
import { EnhancerRegistry, TextareaRegistry } from '../lib/registries'
import { githubPrNewCommentContentScript } from '../playgrounds/github-playground'

const enhancers = new EnhancerRegistry()
const enhancedTextareas = new TextareaRegistry()

function sendEventToBackground(type: 'ENHANCED' | 'DESTROYED', spot: CommentSpot): void {
const message: CommentEvent = {
spot,
type,
}
browser.runtime.sendMessage(message).catch((error) => {
logger.debug('Failed to send event to background:', error)
})
}

enhancedTextareas.setEventHandlers(
(spot) => sendEventToBackground('ENHANCED', spot),
(spot) => sendEventToBackground('DESTROYED', spot),
)

export default defineContentScript({
main() {
if ((CONFIG.MODE as ModeType) === 'PLAYGROUNDS_PR') {
Expand Down
9 changes: 1 addition & 8 deletions browser-extension/src/entrypoints/popup/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,10 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Gitcasso Markdown Assistant</title>
<title>Gitcasso</title>
</head>
<body>
<div id="app">
<div class="header">
<div class="logo">Gitcasso Markdown Assistant</div>
<div class="subtitle">Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).</div>
</div>
<div id="scan-results">
<p>Loading drafts from local storage...</p>
</p>
</div>
<script type="module" src="main.ts"></script>
</body>
Expand Down
110 changes: 90 additions & 20 deletions browser-extension/src/entrypoints/popup/main.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,97 @@
import './style.css'
import { logger } from '../../lib/logger'
import type {
GetOpenSpotsMessage,
GetOpenSpotsResponse,
SwitchToTabMessage,
} from '../../lib/messages'
import { EnhancerRegistry } from '../../lib/registries'
import type { CommentState } from '../background'

document.addEventListener('DOMContentLoaded', async () => {
const statusDiv = document.getElementById('scan-results') as HTMLElement
// Test basic DOM access
try {
const app = document.getElementById('app')!
logger.debug('Found app element:', app)
app.innerHTML = '<div>Script is running...</div>'
} catch (error) {
logger.error('Error accessing DOM:', error)
}

const enhancers = new EnhancerRegistry()

async function getOpenSpots(): Promise<CommentState[]> {
logger.debug('Sending message to background script...')
try {
// get current active tab
const tabs = await browser.tabs.query({ active: true, currentWindow: true })
const tab = tabs[0]

if (!tab?.id) {
statusDiv.textContent = 'Cannot access current tab'
return
}

// send message to content script to get scan results
const results = await browser.tabs.sendMessage(tab.id, {
action: 'getScanResults',
})
if (results) {
// TODO: statusDiv.textContent = {{show drafts}}
}
const message: GetOpenSpotsMessage = { type: 'GET_OPEN_SPOTS' }
const response = (await browser.runtime.sendMessage(message)) as GetOpenSpotsResponse
logger.debug('Received response:', response)
return response.spots || []
} catch (error) {
console.error('Popup error:', error)
statusDiv.textContent = 'Unable to load saved drafts.'
logger.error('Error sending message to background:', error)
return []
}
}

function switchToTab(tabId: number, windowId: number): void {
// Send message to background script to handle tab switching
// This avoids the popup context being destroyed before completion
const message: SwitchToTabMessage = {
tabId,
type: 'SWITCH_TO_TAB',
windowId,
}
browser.runtime.sendMessage(message)
window.close()
}

function createSpotElement(commentState: CommentState): HTMLElement {
const item = document.createElement('div')
item.className = 'spot-item'

logger.debug('Creating spot element for:', commentState.spot)
const enhancer = enhancers.enhancerFor(commentState.spot)
if (!enhancer) {
logger.error('No enhancer found for:', commentState.spot)
logger.error('Only have enhancers for:', enhancers.byType)
}

const title = document.createElement('div')
title.className = 'spot-title'
title.textContent = enhancer.tableTitle(commentState.spot)
item.appendChild(title)
item.addEventListener('click', () => {
switchToTab(commentState.tab.tabId, commentState.tab.windowId)
})
return item
}

async function renderOpenSpots(): Promise<void> {
logger.debug('renderOpenSpots called')
const app = document.getElementById('app')!
const spots = await getOpenSpots()
logger.debug('Got spots:', spots)

if (spots.length === 0) {
app.innerHTML = '<div class="no-spots">No open comment spots</div>'
return
}

const header = document.createElement('h2')
header.textContent = 'Open Comment Spots'
app.appendChild(header)

const list = document.createElement('div')
list.className = 'spots-list'

spots.forEach((spot) => {
list.appendChild(createSpotElement(spot))
})

app.appendChild(list)
}

renderOpenSpots().catch((error) => {
logger.error('Error in renderOpenSpots:', error)
const app = document.getElementById('app')!
app.innerHTML = `<div class="no-spots">Error loading spots: ${error.message}</div>`
})
48 changes: 38 additions & 10 deletions browser-extension/src/entrypoints/popup/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,47 @@ body {
margin: 0;
}

.header {
text-align: center;
margin-bottom: 15px;
h2 {
margin: 0 0 15px 0;
font-size: 16px;
font-weight: 600;
color: #333;
}

.spots-list {
display: flex;
flex-direction: column;
gap: 8px;
}

.spot-item {
padding: 10px;
border: 1px solid #e0e0e0;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
background: white;
}

.spot-item:hover {
background: #f5f5f5;
border-color: #d0d0d0;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.logo {
font-size: 18px;
font-weight: bold;
color: #0066cc;
margin-bottom: 8px;
.spot-title {
font-weight: 500;
color: #333;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.subtitle {
.no-spots {
text-align: center;
color: #666;
font-size: 12px;
padding: 40px 20px;
font-style: italic;
}
14 changes: 13 additions & 1 deletion browser-extension/src/lib/enhancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ export interface CommentSpot {
type: string
}

export interface CommentDraft {
title?: string
body: string
}

export type CommentEventType = 'ENHANCED' | 'LOST_FOCUS' | 'DESTROYED'

export interface CommentEvent {
type: CommentEventType
spot: CommentSpot
draft?: CommentDraft
}

/** Wraps the textareas of a given platform with Gitcasso's enhancements. */
export interface CommentEnhancer<Spot extends CommentSpot = CommentSpot> {
/** Guarantees to only return a type within this list. */
Expand All @@ -30,5 +43,4 @@ export interface CommentEnhancer<Spot extends CommentSpot = CommentSpot> {

tableIcon(spot: Spot): string
tableTitle(spot: Spot): string
buildUrl(spot: Spot): string
}
Loading