-
-
-
Loading drafts from local storage...
-
diff --git a/browser-extension/src/entrypoints/popup/main.ts b/browser-extension/src/entrypoints/popup/main.ts
index cdb6d2d..4ccea9a 100644
--- a/browser-extension/src/entrypoints/popup/main.ts
+++ b/browser-extension/src/entrypoints/popup/main.ts
@@ -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 = '
Script is running...
'
+} catch (error) {
+ logger.error('Error accessing DOM:', error)
+}
+const enhancers = new EnhancerRegistry()
+
+async function getOpenSpots(): Promise
{
+ 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 {
+ logger.debug('renderOpenSpots called')
+ const app = document.getElementById('app')!
+ const spots = await getOpenSpots()
+ logger.debug('Got spots:', spots)
+
+ if (spots.length === 0) {
+ app.innerHTML = 'No open comment spots
'
+ 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 = `Error loading spots: ${error.message}
`
})
diff --git a/browser-extension/src/entrypoints/popup/style.css b/browser-extension/src/entrypoints/popup/style.css
index 40c75e1..0d63390 100644
--- a/browser-extension/src/entrypoints/popup/style.css
+++ b/browser-extension/src/entrypoints/popup/style.css
@@ -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;
}
diff --git a/browser-extension/src/lib/enhancer.ts b/browser-extension/src/lib/enhancer.ts
index e1e86fc..0895454 100644
--- a/browser-extension/src/lib/enhancer.ts
+++ b/browser-extension/src/lib/enhancer.ts
@@ -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 {
/** Guarantees to only return a type within this list. */
@@ -30,5 +43,4 @@ export interface CommentEnhancer {
tableIcon(spot: Spot): string
tableTitle(spot: Spot): string
- buildUrl(spot: Spot): string
}
diff --git a/browser-extension/src/lib/messages.ts b/browser-extension/src/lib/messages.ts
new file mode 100644
index 0000000..e04e8c5
--- /dev/null
+++ b/browser-extension/src/lib/messages.ts
@@ -0,0 +1,80 @@
+import type { CommentDraft, CommentEvent, CommentSpot } from './enhancer'
+
+// Message handler response types
+export const CLOSE_MESSAGE_PORT = false as const // No response will be sent
+export const KEEP_PORT_OPEN = true as const // Response will be sent (possibly async)
+
+// Content -> Background messages (already well-typed as CommentEvent)
+export type ContentToBackgroundMessage = CommentEvent
+
+// Popup -> Background messages
+export interface GetOpenSpotsMessage {
+ type: 'GET_OPEN_SPOTS'
+}
+
+export interface SwitchToTabMessage {
+ type: 'SWITCH_TO_TAB'
+ tabId: number
+ windowId: number
+}
+
+export type PopupToBackgroundMessage = GetOpenSpotsMessage | SwitchToTabMessage
+
+// All messages sent to background
+export type ToBackgroundMessage = ContentToBackgroundMessage | PopupToBackgroundMessage
+
+// Background -> Popup responses
+export interface GetOpenSpotsResponse {
+ spots: Array<{
+ tab: {
+ tabId: number
+ windowId: number
+ }
+ spot: CommentSpot
+ drafts: Array<[number, CommentDraft]>
+ }>
+}
+
+// Type guard functions
+export function isContentToBackgroundMessage(message: any): message is ContentToBackgroundMessage {
+ return (
+ message &&
+ typeof message.type === 'string' &&
+ (message.type === 'ENHANCED' || message.type === 'DESTROYED') &&
+ message.spot
+ )
+}
+
+export function isPopupToBackgroundMessage(message: any): message is PopupToBackgroundMessage {
+ return (
+ message &&
+ typeof message.type === 'string' &&
+ (message.type === 'GET_OPEN_SPOTS' || message.type === 'SWITCH_TO_TAB')
+ )
+}
+
+export function isGetOpenSpotsMessage(message: any): message is GetOpenSpotsMessage {
+ return message && message.type === 'GET_OPEN_SPOTS'
+}
+
+export function isSwitchToTabMessage(message: any): message is SwitchToTabMessage {
+ return (
+ message &&
+ message.type === 'SWITCH_TO_TAB' &&
+ typeof message.tabId === 'number' &&
+ typeof message.windowId === 'number'
+ )
+}
+
+// Message handler types
+export type BackgroundMessageHandler = (
+ message: ToBackgroundMessage,
+ sender: any,
+ sendResponse: (response?: any) => void,
+) => boolean | undefined
+
+export type PopupMessageSender = {
+ sendMessage(
+ message: T,
+ ): Promise
+}
diff --git a/browser-extension/src/lib/registries.ts b/browser-extension/src/lib/registries.ts
index 367533a..b6d8ad9 100644
--- a/browser-extension/src/lib/registries.ts
+++ b/browser-extension/src/lib/registries.ts
@@ -13,6 +13,7 @@ export interface EnhancedTextarea {
export class EnhancerRegistry {
private enhancers = new Set()
private preparedEnhancers = new Set()
+ byType = new Map()
constructor() {
// Register all available handlers
@@ -20,8 +21,15 @@ export class EnhancerRegistry {
this.register(new GitHubPRAddCommentEnhancer())
}
- private register(handler: CommentEnhancer): void {
- this.enhancers.add(handler)
+ private register(enhancer: CommentEnhancer): void {
+ this.enhancers.add(enhancer)
+ for (const spotType of enhancer.forSpotTypes()) {
+ this.byType.set(spotType, enhancer)
+ }
+ }
+
+ enhancerFor(spot: T): CommentEnhancer {
+ return this.byType.get(spot.type)! as CommentEnhancer
}
tryToEnhance(textarea: HTMLTextAreaElement): EnhancedTextarea | null {
@@ -51,15 +59,26 @@ export class EnhancerRegistry {
export class TextareaRegistry {
private textareas = new Map()
+ private onEnhanced?: (spot: CommentSpot) => void
+ private onDestroyed?: (spot: CommentSpot) => void
+
+ setEventHandlers(
+ onEnhanced: (spot: CommentSpot) => void,
+ onDestroyed: (spot: CommentSpot) => void,
+ ): void {
+ this.onEnhanced = onEnhanced
+ this.onDestroyed = onDestroyed
+ }
register(textareaInfo: EnhancedTextarea): void {
this.textareas.set(textareaInfo.textarea, textareaInfo)
- // TODO: register as a draft in progress with the global list
+ this.onEnhanced?.(textareaInfo.spot)
}
unregisterDueToModification(textarea: HTMLTextAreaElement): void {
- if (this.textareas.has(textarea)) {
- // TODO: register as abandoned or maybe submitted with the global list
+ const textareaInfo = this.textareas.get(textarea)
+ if (textareaInfo) {
+ this.onDestroyed?.(textareaInfo.spot)
this.textareas.delete(textarea)
}
}
diff --git a/browser-extension/tests/background.test.ts b/browser-extension/tests/background.test.ts
new file mode 100644
index 0000000..961e1d0
--- /dev/null
+++ b/browser-extension/tests/background.test.ts
@@ -0,0 +1,126 @@
+import { beforeEach, describe, expect, it } from 'vitest'
+import { handleCommentEvent, openSpots } from '../src/entrypoints/background'
+import type { CommentEvent, CommentSpot } from '../src/lib/enhancer'
+
+const mockSender = {
+ tab: {
+ id: 123,
+ windowId: 456,
+ },
+}
+const mockSpot = {
+ type: 'TEST_SPOT',
+ unique_key: 'test-key',
+}
+describe('Background Event Handler', () => {
+ beforeEach(() => {
+ openSpots.clear()
+ })
+ describe('ENHANCED Event', () => {
+ it('should create new comment state when textarea is enhanced', () => {
+ handleCommentEvent(
+ {
+ spot: mockSpot,
+ type: 'ENHANCED',
+ },
+ mockSender,
+ )
+ expect(Array.from(openSpots)).toMatchInlineSnapshot(`
+ [
+ [
+ "test-key",
+ {
+ "drafts": [],
+ "spot": {
+ "type": "TEST_SPOT",
+ "unique_key": "test-key",
+ },
+ "tab": {
+ "tabId": 123,
+ "windowId": 456,
+ },
+ },
+ ],
+ ]
+ `)
+ })
+ it('should not handle ENHANCED event without tab info', () => {
+ const senderWithoutTab = { tab: null }
+ handleCommentEvent(
+ {
+ spot: mockSpot,
+ type: 'ENHANCED',
+ },
+ senderWithoutTab,
+ )
+ expect(openSpots.size).toBe(0)
+ })
+ })
+
+ describe('DESTROYED Event', () => {
+ it('should remove comment state when textarea is destroyed', () => {
+ // First create a state using the actual handler
+ const enhanceMessage: CommentEvent = {
+ spot: mockSpot,
+ type: 'ENHANCED',
+ }
+ handleCommentEvent(enhanceMessage, mockSender)
+ expect(openSpots.size).toBe(1)
+
+ // Then destroy it
+ const destroyMessage: CommentEvent = {
+ spot: mockSpot,
+ type: 'DESTROYED',
+ }
+ handleCommentEvent(destroyMessage, mockSender)
+ expect(openSpots.size).toBe(0)
+ })
+
+ it('should handle DESTROYED event for non-existent state gracefully', () => {
+ const message: CommentEvent = {
+ spot: mockSpot,
+ type: 'DESTROYED',
+ }
+ // Should not throw error
+ handleCommentEvent(message, mockSender)
+ expect(openSpots.size).toBe(0)
+ })
+ })
+
+ describe('Invalid Events', () => {
+ it('should ignore events with unsupported type', () => {
+ const message: CommentEvent = {
+ spot: mockSpot,
+ type: 'LOST_FOCUS',
+ }
+ handleCommentEvent(message, mockSender)
+ expect(openSpots.size).toBe(0)
+ })
+ })
+
+ describe('State Management', () => {
+ it('should handle multiple enhanced textareas from different tabs', () => {
+ const spot1: CommentSpot = { type: 'SPOT1', unique_key: 'key1' }
+ const spot2: CommentSpot = { type: 'SPOT2', unique_key: 'key2' }
+ const sender1 = { tab: { id: 123, windowId: 456 } }
+ const sender2 = { tab: { id: 789, windowId: 456 } }
+ handleCommentEvent({ spot: spot1, type: 'ENHANCED' }, sender1)
+ handleCommentEvent({ spot: spot2, type: 'ENHANCED' }, sender2)
+ expect(openSpots.size).toBe(2)
+ })
+
+ it('should handle same spot from same tab (overwrite)', () => {
+ const message: CommentEvent = {
+ spot: mockSpot,
+ type: 'ENHANCED',
+ }
+
+ // Enhance same spot twice
+ handleCommentEvent(message, mockSender)
+ handleCommentEvent(message, mockSender)
+
+ // Should still be 1 entry (overwritten)
+ expect(openSpots.size).toBe(1)
+ })
+ })
+})
diff --git a/browser-extension/wxt.config.ts b/browser-extension/wxt.config.ts
index edc2589..7a4699a 100644
--- a/browser-extension/wxt.config.ts
+++ b/browser-extension/wxt.config.ts
@@ -11,7 +11,7 @@ export default defineConfig({
128: '/icons/icon-128.png',
},
name: 'Gitcasso',
- permissions: ['activeTab'],
+ permissions: ['activeTab', 'tabs'],
version: '1.0.0',
},
modules: ['@wxt-dev/webextension-polyfill'],