From 525ab07717500c9dd1d971377ee2a8f1237c32d6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 19 Sep 2025 10:46:56 -0700 Subject: [PATCH 01/10] Update dev instructions a bit. --- CONTRIBUTING.md | 95 ++++++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5c6357a..8cc99bf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,23 +16,22 @@ This extension ***must never transmit any data outside the browser***. ## Developer quickstart -### Hotreload development - - `pnpm install` -- `pnpm dev` -- open [`chrome://extensions`](chrome://extensions) -- toggle **Developer mode** (top-right) -- click "Load unpacked" (far left) - - `.output/chrome-mv3-dev` - - if you can't find `.output`, it's probably hidden, `command+shift+period` will show it -- click the puzzle icon next to the url bar, then pin the Gitcasso icon - -### Testing and quality -- `pnpm biome` - runs `biome check` (lint & formatting) -- `pnpm biome:fix` - fixes most of what `biome check` finds -- `pnpm typecheck` - typechecking -- `pnpm test` - vitest -- `pnpm test -u` - updates all snapshots +- to update the popup: + - `pnpm playground` gives react hotreload environment +- to improve comment detection and metadata extraction: + - `pnpm corpus` gives dev environment for gitcasso's behavior on specific pages and page states +- to open a PR: + - `pnpm precommit` fixes formatting and lints (biome), runs typechecking and tests + - no worries if some intermediate commits don't pass +- to run the entire end-to-end browser extension with [WXT](https://wxt.dev/) hotreload: + - `pnpm dev` + - open [`chrome://extensions`](chrome://extensions) + - toggle **Developer mode** (top-right) + - click "Load unpacked" (far left) + - `.output/chrome-mv3-dev` + - if you can't find `.output`, it's probably hidden, `command+shift+period` will show it + - click the puzzle icon next to the url bar, then pin the Gitcasso icon ## How it works @@ -76,46 +75,52 @@ Those `Spot` values get bundled up with the `HTMLTextAreaElement` itself into an 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 +## Test corpus -- `pnpm playground` gives you a test environment where you can tinker with the popup with various test data, supports hot reload -- `pnpm corpus` gives you recordings of various web pages which you can see with and without enhancement by the browser extension +We maintain a corpus of test pages in two formats for testing the browser extension: +- `html` created by the [SingleFile](https://chromewebstore.google.com/detail/singlefile/mpiodijhokgodhhofbcjdecpffjipkle) browser extension + - allows snapshotting the DOM at an intermediate step in the user's experience + - most interactivity will be broken +- `har` recording of all network traffic of an initial pageload + - limited to initial page load, but most interactivity will work -### Test Corpus +This corpus of pages is used to power snapshot tests and interactive dev environments. -We maintain a corpus of test pages in two formats for testing the browser extension: +### Viewing corpus -#### HAR Corpus (Automated) +- **DISABLE GITCASSO IN YOUR BROWSER!!** +- Run `pnpm corpus` to start the test server at http://localhost:3001 +- Select any corpus file to view in two modes: + - **Clean**: Original unaltered page + - **Gitcasso**: Page with extension injected for testing + - it shows all data Gitcasso is extracting (if any) + - click the rebuild button to test changes to the Gitcasso source -- For testing initial page loads and network requests -- HAR recordings live in `tests/corpus/*.har`, complete recordings of the network requests of a single page load -- You can add or change URLs in `tests/corpus/_corpus-index.ts` -- **Recording new HAR files:** - - `npx playwright codegen https://github.com/login --save-storage=playwright/.auth/gh.json` will store new auth tokens - - login manually, then close the browser - - ***these cookies are very sensitive! we only run this script using a test account that has no permissions or memberships to anything, recommend you do the same!*** - - `pnpm corpus:har:record` records new HAR files using those auth tokens (it needs args, run it with no args for docs) - - DO NOT COMMIT AND PUSH NEW OR CHANGED HAR files! - - we try to sanitize these (see `corpus-har-record.ts` for details) but there may be important PII in them - - if you need new HAR files for something, let us know and we will generate them ourselves using a dummy account - - IF YOUR PR CHANGES OR ADDS HAR FILES WE WILL CLOSE IT. Ask for HAR files and we'll be happy to generate clean ones you can test against. +### Unit testing against corpus + +- `gh-detection.test.ts` does snapshot testing on the data which gitcasso extracts +- `gh-ui.test.ts` does snapshot testing on the popup table decoration for all data extracted from the snapshots -#### HTML Corpus (Manual) +### Adding HTML to the corpus (Manual) - For testing post-interaction states (e.g., expanded textareas, modal dialogs, dynamic content) -- HTML snapshots live in `tests/corpus/*.html`, manually captured using SingleFile browser extension -- All assets are inlined in a single HTML file by SingleFile -- **Creating new HTML corpus files:** +- HTML snapshots live in `tests/corpus/*.html`, manually captured using SingleFile +- how-to 1. Navigate to the desired page state (click buttons, expand textareas, etc.) 2. Use SingleFile browser extension to save the complete page - 3. Save the `.html` file to `tests/corpus/html/` with a descriptive name + 3. Save the `.html` file to `tests/corpus/` with a descriptive slug 4. Add an entry to `tests/corpus/_corpus-index.ts` with `type: 'html'` and a description of the captured state 5. Feel free to contribute these if you want, but be mindful that they will be part of our immutable git history -#### Viewing Corpus Files +### Adding HAR to the corpus (Automated) -- Run `pnpm corpus` to start the test server at http://localhost:3001 -- Select any corpus file to view in two modes: - - **Clean**: Original page without extension - - **Gitcasso**: Page with extension injected for testing -- Both HAR and HTML corpus types are supported +- For testing initial page loads and network requests +- HAR recordings live in `tests/corpus/*.har`, complete recordings of the network requests of a single page load +- how-to + - update `tests/corpus/_corpus-index.ts` with a descriptive slug and URL which you want to add + - `npx playwright codegen https://github.com/login --save-storage=playwright/.auth/gh.json` will store new auth tokens + - login manually, then close the browser + - ***these cookies are very sensitive! we only run this script using a test account that has no permissions or memberships to anything, recommend you do the same!*** + - `pnpm corpus:har:record {slug}` records new HAR files using those auth tokens (it needs args, run it with no args for docs) + - **CONTRIBUTING THESE IS RISKY!** + - we try to sanitize these (see `corpus-har-record.ts` for details) but there may be important PII in them, which is why we only use a test account diff --git a/package.json b/package.json index 2623443..6955aeb 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "build": "pnpm run build:overtype && wxt build", "build:dev": "pnpm run build:overtype && wxt build --mode development", "build:firefox": "wxt build -b firefox", - "precommit": "npm run biome:fix && npm run typecheck && npm run test", + "precommit": "pnpm biome:fix && pnpm typecheck && pnpm test", "typecheck": "tsc --noEmit", "dev": "wxt", "dev:firefox": "wxt -b firefox", From e343ebd97acfcb5f3c61d0e3c5ae61c99a9167ec Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 19 Sep 2025 10:59:20 -0700 Subject: [PATCH 02/10] Send draft content on enhanced/unenhanced. --- src/entrypoints/content.ts | 15 +++++++++++---- src/lib/registries.ts | 12 ++++++------ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/entrypoints/content.ts b/src/entrypoints/content.ts index 5dc5122..0ef1cf4 100644 --- a/src/entrypoints/content.ts +++ b/src/entrypoints/content.ts @@ -20,19 +20,26 @@ function detectLocation(): StrippedLocation { return result } -function sendEventToBackground(type: 'ENHANCED' | 'DESTROYED', spot: CommentSpot): void { +function sendEventToBackground( + type: 'ENHANCED' | 'DESTROYED', + spot: CommentSpot, + draft: string, +): void { const message: CommentEvent = { + draft, spot, type, } browser.runtime.sendMessage(message).catch((error) => { - logger.debug('Failed to send event to background:', error) + logger.error('Failed to send event to background:', error) }) } enhancedTextareas.setEventHandlers( - (spot) => sendEventToBackground('ENHANCED', spot), - (spot) => sendEventToBackground('DESTROYED', spot), + (textareaInfo) => + sendEventToBackground('ENHANCED', textareaInfo.spot, textareaInfo.textarea.value), + (textareaInfo) => + sendEventToBackground('DESTROYED', textareaInfo.spot, textareaInfo.textarea.value), ) export default defineContentScript({ diff --git a/src/lib/registries.ts b/src/lib/registries.ts index 859b453..1beecba 100644 --- a/src/lib/registries.ts +++ b/src/lib/registries.ts @@ -101,12 +101,12 @@ export class EnhancerRegistry { export class TextareaRegistry { private textareas = new Map() - private onEnhanced?: (spot: CommentSpot) => void - private onDestroyed?: (spot: CommentSpot) => void + private onEnhanced?: (textareaInfo: EnhancedTextarea) => void + private onDestroyed?: (textareaInfo: EnhancedTextarea) => void setEventHandlers( - onEnhanced: (spot: CommentSpot) => void, - onDestroyed: (spot: CommentSpot) => void, + onEnhanced: (textareaInfo: EnhancedTextarea) => void, + onDestroyed: (textareaInfo: EnhancedTextarea) => void, ): void { this.onEnhanced = onEnhanced this.onDestroyed = onDestroyed @@ -114,13 +114,13 @@ export class TextareaRegistry { register(textareaInfo: EnhancedTextarea): void { this.textareas.set(textareaInfo.textarea, textareaInfo) - this.onEnhanced?.(textareaInfo.spot) + this.onEnhanced?.(textareaInfo) } unregisterDueToModification(textarea: HTMLTextAreaElement): void { const textareaInfo = this.textareas.get(textarea) if (textareaInfo) { - this.onDestroyed?.(textareaInfo.spot) + this.onDestroyed?.(textareaInfo) this.textareas.delete(textarea) } } From 425ca55d12f2f7fa26151359089e014b7287c389 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 19 Sep 2025 11:06:49 -0700 Subject: [PATCH 03/10] Refactor around just `sendEvent` --- src/entrypoints/content.ts | 20 +++----------------- src/lib/registries.ts | 21 ++++++++++++++------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/entrypoints/content.ts b/src/entrypoints/content.ts index 0ef1cf4..ed3f767 100644 --- a/src/entrypoints/content.ts +++ b/src/entrypoints/content.ts @@ -1,4 +1,4 @@ -import type { CommentEvent, CommentSpot, StrippedLocation } from '../lib/enhancer' +import type { CommentEvent, StrippedLocation } from '../lib/enhancer' import { logger } from '../lib/logger' import { EnhancerRegistry, TextareaRegistry } from '../lib/registries' @@ -20,27 +20,13 @@ function detectLocation(): StrippedLocation { return result } -function sendEventToBackground( - type: 'ENHANCED' | 'DESTROYED', - spot: CommentSpot, - draft: string, -): void { - const message: CommentEvent = { - draft, - spot, - type, - } +function sendEventToBackground(message: CommentEvent): void { browser.runtime.sendMessage(message).catch((error) => { logger.error('Failed to send event to background:', error) }) } -enhancedTextareas.setEventHandlers( - (textareaInfo) => - sendEventToBackground('ENHANCED', textareaInfo.spot, textareaInfo.textarea.value), - (textareaInfo) => - sendEventToBackground('DESTROYED', textareaInfo.spot, textareaInfo.textarea.value), -) +enhancedTextareas.setCommentEventSender(sendEventToBackground) export default defineContentScript({ main() { diff --git a/src/lib/registries.ts b/src/lib/registries.ts index 1beecba..5c71276 100644 --- a/src/lib/registries.ts +++ b/src/lib/registries.ts @@ -1,6 +1,6 @@ import type { OverTypeInstance } from 'overtype' import OverType from 'overtype' -import type { CommentEnhancer, CommentSpot, StrippedLocation } from './enhancer' +import type { CommentEnhancer, CommentEvent, CommentSpot, StrippedLocation } from './enhancer' import { CommentEnhancerMissing } from './enhancers/CommentEnhancerMissing' import { GitHubEditEnhancer } from './enhancers/github/GitHubEditEnhancer' import { GitHubIssueAppendEnhancer } from './enhancers/github/GitHubIssueAppendEnhancer' @@ -104,12 +104,19 @@ export class TextareaRegistry { private onEnhanced?: (textareaInfo: EnhancedTextarea) => void private onDestroyed?: (textareaInfo: EnhancedTextarea) => void - setEventHandlers( - onEnhanced: (textareaInfo: EnhancedTextarea) => void, - onDestroyed: (textareaInfo: EnhancedTextarea) => void, - ): void { - this.onEnhanced = onEnhanced - this.onDestroyed = onDestroyed + setCommentEventSender(sendEvent: (event: CommentEvent) => void): void { + this.onEnhanced = (textareaInfo) => + sendEvent({ + draft: textareaInfo.textarea.value, + spot: textareaInfo.spot, + type: 'ENHANCED', + }) + this.onDestroyed = (textareaInfo) => + sendEvent({ + draft: textareaInfo.textarea.value, + spot: textareaInfo.spot, + type: 'DESTROYED', + }) } register(textareaInfo: EnhancedTextarea): void { From f9b6e8eb4be53df7be8ef49911b765a2fcb99aaa Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 19 Sep 2025 11:06:59 -0700 Subject: [PATCH 04/10] Add a slash comment for fixing up precommit --- .claude/commands/precommit.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .claude/commands/precommit.md diff --git a/.claude/commands/precommit.md b/.claude/commands/precommit.md new file mode 100644 index 0000000..f20fadd --- /dev/null +++ b/.claude/commands/precommit.md @@ -0,0 +1,4 @@ +1. run `pnpm precommit` +2. if tests fail, run `pnpm test -u` might fix them +3. fix other problems manually +4. go back to step 1: `pnpm precommit` \ No newline at end of file From 2f4186076b3af22f3f5127c2f538b943ea08722a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 19 Sep 2025 11:11:54 -0700 Subject: [PATCH 05/10] Refactor around a single event sender. --- src/lib/registries.ts | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/lib/registries.ts b/src/lib/registries.ts index 5c71276..88d2de3 100644 --- a/src/lib/registries.ts +++ b/src/lib/registries.ts @@ -101,33 +101,29 @@ export class EnhancerRegistry { export class TextareaRegistry { private textareas = new Map() - private onEnhanced?: (textareaInfo: EnhancedTextarea) => void - private onDestroyed?: (textareaInfo: EnhancedTextarea) => void + private sendEvent: (event: CommentEvent) => void = () => {} setCommentEventSender(sendEvent: (event: CommentEvent) => void): void { - this.onEnhanced = (textareaInfo) => - sendEvent({ - draft: textareaInfo.textarea.value, - spot: textareaInfo.spot, - type: 'ENHANCED', - }) - this.onDestroyed = (textareaInfo) => - sendEvent({ - draft: textareaInfo.textarea.value, - spot: textareaInfo.spot, - type: 'DESTROYED', - }) + this.sendEvent = sendEvent } register(textareaInfo: EnhancedTextarea): void { this.textareas.set(textareaInfo.textarea, textareaInfo) - this.onEnhanced?.(textareaInfo) + this.sendEvent({ + draft: textareaInfo.textarea.value, + spot: textareaInfo.spot, + type: 'ENHANCED', + }) } unregisterDueToModification(textarea: HTMLTextAreaElement): void { const textareaInfo = this.textareas.get(textarea) if (textareaInfo) { - this.onDestroyed?.(textareaInfo) + this.sendEvent({ + draft: textareaInfo.textarea.value, + spot: textareaInfo.spot, + type: 'DESTROYED', + }) this.textareas.delete(textarea) } } From 2f2431b676f85f6958373a774f263b5c4f21e794 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 19 Sep 2025 11:13:08 -0700 Subject: [PATCH 06/10] more refactor --- src/lib/registries.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/lib/registries.ts b/src/lib/registries.ts index 88d2de3..b0be87a 100644 --- a/src/lib/registries.ts +++ b/src/lib/registries.ts @@ -107,21 +107,21 @@ export class TextareaRegistry { this.sendEvent = sendEvent } - register(textareaInfo: EnhancedTextarea): void { - this.textareas.set(textareaInfo.textarea, textareaInfo) + register(enhanced: EnhancedTextarea): void { + this.textareas.set(enhanced.textarea, enhanced) this.sendEvent({ - draft: textareaInfo.textarea.value, - spot: textareaInfo.spot, + draft: enhanced.textarea.value, + spot: enhanced.spot, type: 'ENHANCED', }) } unregisterDueToModification(textarea: HTMLTextAreaElement): void { - const textareaInfo = this.textareas.get(textarea) - if (textareaInfo) { + const enhanced = this.textareas.get(textarea) + if (enhanced) { this.sendEvent({ - draft: textareaInfo.textarea.value, - spot: textareaInfo.spot, + draft: enhanced.textarea.value, + spot: enhanced.spot, type: 'DESTROYED', }) this.textareas.delete(textarea) @@ -131,8 +131,4 @@ export class TextareaRegistry { get(textarea: HTMLTextAreaElement): EnhancedTextarea | undefined { return this.textareas.get(textarea) } - - getAllEnhanced(): EnhancedTextarea[] { - return Array.from(this.textareas.values()) - } } From 28d2c588f0bc30470d608bd66ffd3053f32ec232 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 19 Sep 2025 11:25:18 -0700 Subject: [PATCH 07/10] `TextareaRegistry` now sends events with less verbosity. --- src/lib/registries.ts | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/src/lib/registries.ts b/src/lib/registries.ts index b0be87a..e84ffe1 100644 --- a/src/lib/registries.ts +++ b/src/lib/registries.ts @@ -1,6 +1,12 @@ import type { OverTypeInstance } from 'overtype' import OverType from 'overtype' -import type { CommentEnhancer, CommentEvent, CommentSpot, StrippedLocation } from './enhancer' +import type { + CommentEnhancer, + CommentEvent, + CommentEventType, + CommentSpot, + StrippedLocation, +} from './enhancer' import { CommentEnhancerMissing } from './enhancers/CommentEnhancerMissing' import { GitHubEditEnhancer } from './enhancers/github/GitHubEditEnhancer' import { GitHubIssueAppendEnhancer } from './enhancers/github/GitHubIssueAppendEnhancer' @@ -101,29 +107,32 @@ export class EnhancerRegistry { export class TextareaRegistry { private textareas = new Map() - private sendEvent: (event: CommentEvent) => void = () => {} + private eventSender: (event: CommentEvent) => void = () => {} setCommentEventSender(sendEvent: (event: CommentEvent) => void): void { - this.sendEvent = sendEvent + this.eventSender = sendEvent } - register(enhanced: EnhancedTextarea): void { - this.textareas.set(enhanced.textarea, enhanced) - this.sendEvent({ + private sendEvent(eventType: CommentEventType, enhanced: EnhancedTextarea): void { + this.eventSender({ draft: enhanced.textarea.value, spot: enhanced.spot, - type: 'ENHANCED', + type: eventType, + }) + } + + register(enhanced: EnhancedTextarea): void { + this.textareas.set(enhanced.textarea, enhanced) + enhanced.textarea.addEventListener('blur', () => { + this.sendEvent('LOST_FOCUS', enhanced) }) + this.sendEvent('ENHANCED', enhanced) } unregisterDueToModification(textarea: HTMLTextAreaElement): void { const enhanced = this.textareas.get(textarea) if (enhanced) { - this.sendEvent({ - draft: enhanced.textarea.value, - spot: enhanced.spot, - type: 'DESTROYED', - }) + this.sendEvent('DESTROYED', enhanced) this.textareas.delete(textarea) } } From ed12be45bbf970a920151353fee2906d5f1062eb Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 19 Sep 2025 11:26:45 -0700 Subject: [PATCH 08/10] Use typescript exhaustiveness checking when processing CommentEvent. --- src/entrypoints/background.ts | 37 +++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/entrypoints/background.ts b/src/entrypoints/background.ts index d005150..e9e4812 100644 --- a/src/entrypoints/background.ts +++ b/src/entrypoints/background.ts @@ -1,4 +1,4 @@ -import type { CommentEvent, CommentSpot } from '@/lib/enhancer' +import type { CommentEvent, CommentEventType, CommentSpot } from '@/lib/enhancer' import { type DraftStats, statsFor } from '@/lib/enhancers/draft-stats' import { logger } from '@/lib/logger' import type { GetTableRowsResponse, ToBackgroundMessage } from '@/lib/messages' @@ -38,12 +38,14 @@ export const openSpots = new Map() export function handleCommentEvent(message: CommentEvent, sender: any): boolean { logger.debug('received comment event', message) - if ( - (message.type === 'ENHANCED' || message.type === 'DESTROYED') && - sender.tab?.id && - sender.tab?.windowId - ) { - if (message.type === 'ENHANCED') { + + // Only process events with valid tab information + if (!sender.tab?.id || !sender.tab?.windowId) { + return CLOSE_MESSAGE_PORT + } + + switch (message.type) { + case 'ENHANCED': { const commentState: CommentStorage = { drafts: [[Date.now(), message.draft || '']], sentOn: null, @@ -55,12 +57,27 @@ export function handleCommentEvent(message: CommentEvent, sender: any): boolean trashedOn: null, } openSpots.set(message.spot.unique_key, commentState) - } else if (message.type === 'DESTROYED') { + break + } + case 'DESTROYED': { openSpots.delete(message.spot.unique_key) - } else { - throw new Error(`Unhandled comment event type: ${message.type}`) + break + } + case 'LOST_FOCUS': { + // Update the draft content for existing comment state + const existingState = openSpots.get(message.spot.unique_key) + if (existingState) { + existingState.drafts.push([Date.now(), message.draft || '']) + } + break + } + default: { + // TypeScript exhaustiveness check - will error if we miss any CommentEventType + const exhaustiveCheck: never = message.type satisfies CommentEventType + throw new Error(`Unhandled comment event type: ${exhaustiveCheck}`) } } + return CLOSE_MESSAGE_PORT } From 4e4e5a6bd341d271ae8e2773ad53f5bb4975c126 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 19 Sep 2025 11:30:32 -0700 Subject: [PATCH 09/10] Enforce exhaustiveness here too. --- src/lib/messages.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/lib/messages.ts b/src/lib/messages.ts index 09d0298..da7fa54 100644 --- a/src/lib/messages.ts +++ b/src/lib/messages.ts @@ -1,5 +1,5 @@ import type { CommentTableRow } from '@/entrypoints/background' -import type { CommentEvent } from './enhancer' +import type { CommentEvent, CommentEventType } from './enhancer' // Message handler response types export const CLOSE_MESSAGE_PORT = false as const // No response will be sent @@ -29,12 +29,24 @@ export interface GetTableRowsResponse { rows: CommentTableRow[] } +// Exhaustive list of valid comment event types - TypeScript will error if CommentEventType changes +const COMMENT_EVENT_TYPES = { + DESTROYED: true, + ENHANCED: true, + LOST_FOCUS: true, +} as const satisfies Record + +// Helper function to check if a string is a valid CommentEventType +function isValidCommentEventType(type: string): type is CommentEventType { + return type in COMMENT_EVENT_TYPES +} + // Type guard functions export function isContentToBackgroundMessage(message: any): message is ContentToBackgroundMessage { return ( message && typeof message.type === 'string' && - (message.type === 'ENHANCED' || message.type === 'DESTROYED') && + isValidCommentEventType(message.type) && message.spot ) } From 6113106454b40001f0f1d9ad1a50901a9945b32e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Fri, 19 Sep 2025 11:50:09 -0700 Subject: [PATCH 10/10] Send update on tabLostFocus. --- src/entrypoints/content.ts | 8 ++++++++ src/lib/registries.ts | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/src/entrypoints/content.ts b/src/entrypoints/content.ts index ed3f767..903f4b5 100644 --- a/src/entrypoints/content.ts +++ b/src/entrypoints/content.ts @@ -40,6 +40,14 @@ export default defineContentScript({ childList: true, subtree: true, }) + + // Listen for tab visibility changes to capture draft content when switching tabs + document.addEventListener('visibilitychange', () => { + if (document.hidden) { + enhancedTextareas.tabLostFocus() + } + }) + logger.debug('Extension loaded with', enhancers.getEnhancerCount(), 'handlers') }, matches: [''], diff --git a/src/lib/registries.ts b/src/lib/registries.ts index e84ffe1..eb12d96 100644 --- a/src/lib/registries.ts +++ b/src/lib/registries.ts @@ -140,4 +140,10 @@ export class TextareaRegistry { get(textarea: HTMLTextAreaElement): EnhancedTextarea | undefined { return this.textareas.get(textarea) } + + tabLostFocus(): void { + for (const enhanced of this.textareas.values()) { + this.sendEvent('LOST_FOCUS', enhanced) + } + } }