From 85ec3b940caa07bb256706fe6edeed0a0adfb639 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 14:27:11 -0700 Subject: [PATCH 01/54] wip2 --- browser-extension/src/datamodel/textarea-handler.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 browser-extension/src/datamodel/textarea-handler.ts diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts new file mode 100644 index 0000000..e69de29 From 3883abe5fb0d0ac799f081e8a98cd09860e28baf Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 15:38:10 -0700 Subject: [PATCH 02/54] First cut. --- .../src/datamodel/textarea-handler.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts index e69de29..0ed0661 100644 --- a/browser-extension/src/datamodel/textarea-handler.ts +++ b/browser-extension/src/datamodel/textarea-handler.ts @@ -0,0 +1,80 @@ +export type CommentType = + | 'GH_ISSUE_NEW' + | 'GH_PR_NEW' + | 'GH_ISSUE_ADD_COMMENT' + | 'GH_ISSUE_EDIT_COMMENT' + | 'GH_PR_ADD_COMMENT' + | 'GH_PR_EDIT_COMMENT' + | 'GH_PR_CODE_COMMENT' + | 'REDDIT_POST_NEW' + | 'REDDIT_COMMENT_NEW' + | 'REDDIT_COMMENT_EDIT' + | 'GL_ISSUE_NEW' + | 'GL_MR_NEW' + | 'GL_ISSUE_ADD_COMMENT' + | 'GL_MR_ADD_COMMENT' + | 'BB_ISSUE_NEW' + | 'BB_PR_NEW' + | 'BB_ISSUE_ADD_COMMENT' + | 'BB_PR_ADD_COMMENT'; + +export interface CommentContext { + unique_key: string; +} + +export interface TextareaInfo { + element: HTMLTextAreaElement; + type: CommentType; + context: T; +} + +export interface TextareaHandler { + // Content script functionality + identify(): TextareaInfo[]; + readContent(textarea: HTMLTextAreaElement): string; + setContent(textarea: HTMLTextAreaElement, content: string): void; + onSubmit(textarea: HTMLTextAreaElement, callback: (success: boolean) => void): void; + + // Context extraction + extractContext(textarea: HTMLTextAreaElement): T | null; + determineType(textarea: HTMLTextAreaElement): CommentType | null; + + // Popup functionality helpers + generateDisplayTitle(context: T): string; + generateIcon(type: CommentType): string; + buildUrl(context: T, withDraft?: boolean): string; +} + +export abstract class BaseTextareaHandler implements TextareaHandler { + protected domain: string; + + constructor(domain: string) { + this.domain = domain; + } + + abstract identify(): TextareaInfo[]; + abstract extractContext(textarea: HTMLTextAreaElement): T | null; + abstract determineType(textarea: HTMLTextAreaElement): CommentType | null; + abstract generateDisplayTitle(context: T): string; + abstract generateIcon(type: CommentType): string; + abstract buildUrl(context: T, withDraft?: boolean): string; + + readContent(textarea: HTMLTextAreaElement): string { + return textarea.value; + } + + setContent(textarea: HTMLTextAreaElement, content: string): void { + textarea.value = content; + textarea.dispatchEvent(new Event('input', { bubbles: true })); + textarea.dispatchEvent(new Event('change', { bubbles: true })); + } + + onSubmit(textarea: HTMLTextAreaElement, callback: (success: boolean) => void): void { + const form = textarea.closest('form'); + if (form) { + form.addEventListener('submit', () => { + setTimeout(() => callback(true), 100); + }, { once: true }); + } + } +} \ No newline at end of file From 3e676e3dd32ee1d7bf7c7d14c0215f29053802c4 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 15:59:44 -0700 Subject: [PATCH 03/54] Progress --- .../src/datamodel/handler-registry.ts | 49 +++++ .../src/datamodel/textarea-handler.ts | 4 + browser-extension/src/entrypoints/content.ts | 21 ++- .../src/handlers/github-handler.ts | 174 ++++++++++++++++++ .../src/handlers/reddit-handler.ts | 153 +++++++++++++++ 5 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 browser-extension/src/datamodel/handler-registry.ts create mode 100644 browser-extension/src/handlers/github-handler.ts create mode 100644 browser-extension/src/handlers/reddit-handler.ts diff --git a/browser-extension/src/datamodel/handler-registry.ts b/browser-extension/src/datamodel/handler-registry.ts new file mode 100644 index 0000000..8d83c0a --- /dev/null +++ b/browser-extension/src/datamodel/handler-registry.ts @@ -0,0 +1,49 @@ +import { CommentType, CommentContext, TextareaHandler, TextareaInfo } from './textarea-handler'; +import { GitHubHandler } from '../handlers/github-handler'; +import { RedditHandler } from '../handlers/reddit-handler'; + +export class HandlerRegistry { + private handlers = new Set>(); + + constructor() { + // Register all available handlers + this.register(new GitHubHandler()); + this.register(new RedditHandler()); + } + + private register(handler: TextareaHandler): void { + this.handlers.add(handler); + } + + getHandlerForType(type: CommentType): TextareaHandler | null { + for (const handler of this.handlers) { + if (handler.forCommentTypes().includes(type)) { + return handler; + } + } + return null; + } + + identifyAll(): TextareaInfo[] { + const allTextareas: TextareaInfo[] = []; + + for (const handler of this.handlers) { + try { + const textareas = handler.identify(); + allTextareas.push(...textareas); + } catch (error) { + console.warn('Handler failed to identify textareas:', error); + } + } + + return allTextareas; + } + + getAllHandlers(): TextareaHandler[] { + return Array.from(this.handlers); + } + + getCommentTypesForHandler(handler: TextareaHandler): CommentType[] { + return handler.forCommentTypes(); + } +} \ No newline at end of file diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts index 0ed0661..02e04af 100644 --- a/browser-extension/src/datamodel/textarea-handler.ts +++ b/browser-extension/src/datamodel/textarea-handler.ts @@ -29,6 +29,9 @@ export interface TextareaInfo { } export interface TextareaHandler { + // Handler metadata + forCommentTypes(): CommentType[]; + // Content script functionality identify(): TextareaInfo[]; readContent(textarea: HTMLTextAreaElement): string; @@ -52,6 +55,7 @@ export abstract class BaseTextareaHandler[]; abstract extractContext(textarea: HTMLTextAreaElement): T | null; abstract determineType(textarea: HTMLTextAreaElement): CommentType | null; diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index c730dce..ff85d70 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -1,6 +1,9 @@ import { CONFIG } from './content/config' import { logger } from './content/logger' import { injectStyles } from './content/styles' +import { HandlerRegistry } from '../datamodel/handler-registry' + +const registry = new HandlerRegistry() export default defineContentScript({ main() { @@ -13,7 +16,7 @@ export default defineContentScript({ childList: true, subtree: true, }) - logger.debug('Extension loaded') + logger.debug('Extension loaded with', registry.getAllHandlers().length, 'handlers') }, matches: [''], runAt: 'document_end', @@ -24,9 +27,16 @@ function handleMutations(mutations: MutationRecord[]): void { for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as Element - if (element.tagName === 'textarea') { + if (element.tagName === 'TEXTAREA') { initializeMaybe(element as HTMLTextAreaElement) } + // Also check for textareas within added subtrees + const textareas = element.querySelectorAll?.('textarea') + if (textareas) { + for (const textarea of textareas) { + initializeMaybe(textarea) + } + } } } } @@ -37,6 +47,13 @@ function initializeMaybe(textarea: HTMLTextAreaElement) { logger.debug('activating textarea {}', textarea) injectStyles() textarea.classList.add(CONFIG.ADDED_OVERTYPE_CLASS) + + // Use registry to identify and handle this textarea + const textareaInfos = registry.identifyAll().filter(info => info.element === textarea) + for (const info of textareaInfos) { + logger.debug('Identified textarea:', info.type, info.context.unique_key) + // TODO: Set up textarea monitoring and draft saving + } } else { logger.debug('already activated textarea {}', textarea) } diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/handlers/github-handler.ts new file mode 100644 index 0000000..55775f8 --- /dev/null +++ b/browser-extension/src/handlers/github-handler.ts @@ -0,0 +1,174 @@ +import { CommentType, CommentContext, BaseTextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; + +export interface GitHubContext extends CommentContext { + domain: string; + slug: string; // owner/repo + number?: number; // issue/PR number + commentId?: string; // for editing existing comments +} + +export class GitHubHandler extends BaseTextareaHandler { + constructor() { + super('github.com'); + } + + forCommentTypes(): CommentType[] { + return [ + 'GH_ISSUE_NEW', + 'GH_PR_NEW', + 'GH_ISSUE_ADD_COMMENT', + 'GH_ISSUE_EDIT_COMMENT', + 'GH_PR_ADD_COMMENT', + 'GH_PR_EDIT_COMMENT', + 'GH_PR_CODE_COMMENT' + ]; + } + + identify(): TextareaInfo[] { + const textareas = document.querySelectorAll('textarea'); + const results: TextareaInfo[] = []; + + for (const textarea of textareas) { + const type = this.determineType(textarea); + const context = this.extractContext(textarea); + + if (type && context) { + results.push({ element: textarea, type, context }); + } + } + + return results; + } + + extractContext(textarea: HTMLTextAreaElement): GitHubContext | null { + const url = window.location.href; + const pathname = window.location.pathname; + + // Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456 + const match = pathname.match(/^\/([^\/]+)\/([^\/]+)(?:\/(issues|pull)\/(\d+))?/); + if (!match) return null; + + const [, owner, repo, type, numberStr] = match; + const slug = `${owner}/${repo}`; + const number = numberStr ? parseInt(numberStr, 10) : undefined; + + // Generate unique key based on context + let unique_key = `github:${slug}`; + if (number) { + unique_key += `:${type}:${number}`; + } else { + unique_key += ':new'; + } + + // Check if editing existing comment + const commentId = this.getCommentId(textarea); + if (commentId) { + unique_key += `:edit:${commentId}`; + } + + return { + unique_key, + domain: window.location.hostname, + slug, + number, + commentId + }; + } + + determineType(textarea: HTMLTextAreaElement): CommentType | null { + const pathname = window.location.pathname; + + // New issue + if (pathname.includes('/issues/new')) { + return 'GH_ISSUE_NEW'; + } + + // New PR + if (pathname.includes('/compare/') || pathname.endsWith('/compare')) { + return 'GH_PR_NEW'; + } + + // Check if we're on an issue or PR page + const match = pathname.match(/\/(issues|pull)\/(\d+)/); + if (!match) return null; + + const [, type] = match; + const isEditingComment = this.getCommentId(textarea) !== null; + + if (type === 'issues') { + return isEditingComment ? 'GH_ISSUE_EDIT_COMMENT' : 'GH_ISSUE_ADD_COMMENT'; + } else { + // Check if it's a code comment (in Files Changed tab) + const isCodeComment = textarea.closest('.js-inline-comment-form') !== null || + textarea.closest('[data-path]') !== null; + + if (isCodeComment) { + return 'GH_PR_CODE_COMMENT'; + } + + return isEditingComment ? 'GH_PR_EDIT_COMMENT' : 'GH_PR_ADD_COMMENT'; + } + } + + generateDisplayTitle(context: GitHubContext): string { + const { slug, number, commentId } = context; + + if (commentId) { + return `Edit comment in ${slug}${number ? ` #${number}` : ''}`; + } + + if (number) { + return `Comment on ${slug} #${number}`; + } + + return `New ${window.location.pathname.includes('/issues/') ? 'issue' : 'PR'} in ${slug}`; + } + + generateIcon(type: CommentType): string { + switch (type) { + case 'GH_ISSUE_NEW': + case 'GH_ISSUE_ADD_COMMENT': + case 'GH_ISSUE_EDIT_COMMENT': + return '🐛'; // Issue icon + case 'GH_PR_NEW': + case 'GH_PR_ADD_COMMENT': + case 'GH_PR_EDIT_COMMENT': + return '🔄'; // PR icon + case 'GH_PR_CODE_COMMENT': + return '💬'; // Code comment icon + default: + return '📝'; // Generic comment icon + } + } + + buildUrl(context: GitHubContext, withDraft?: boolean): string { + const baseUrl = `https://${context.domain}/${context.slug}`; + + if (context.number) { + const type = window.location.pathname.includes('/issues/') ? 'issues' : 'pull'; + return `${baseUrl}/${type}/${context.number}${context.commentId ? `#issuecomment-${context.commentId}` : ''}`; + } + + return baseUrl; + } + + private getCommentId(textarea: HTMLTextAreaElement): string | null { + // Look for edit comment form indicators + const commentForm = textarea.closest('[data-comment-id]'); + if (commentForm) { + return commentForm.getAttribute('data-comment-id'); + } + + const editForm = textarea.closest('.js-comment-edit-form'); + if (editForm) { + const commentContainer = editForm.closest('.js-comment-container'); + if (commentContainer) { + const id = commentContainer.getAttribute('data-gid') || + commentContainer.getAttribute('id'); + return id ? id.replace('issuecomment-', '') : null; + } + } + + return null; + } +} \ No newline at end of file diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/handlers/reddit-handler.ts new file mode 100644 index 0000000..465cb38 --- /dev/null +++ b/browser-extension/src/handlers/reddit-handler.ts @@ -0,0 +1,153 @@ +import { CommentType, CommentContext, BaseTextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; + +export interface RedditContext extends CommentContext { + subreddit: string; + postId?: string; + commentId?: string; // for editing existing comments +} + +export class RedditHandler extends BaseTextareaHandler { + constructor() { + super('reddit.com'); + } + + forCommentTypes(): CommentType[] { + return [ + 'REDDIT_POST_NEW', + 'REDDIT_COMMENT_NEW', + 'REDDIT_COMMENT_EDIT' + ]; + } + + identify(): TextareaInfo[] { + const textareas = document.querySelectorAll('textarea'); + const results: TextareaInfo[] = []; + + for (const textarea of textareas) { + const type = this.determineType(textarea); + const context = this.extractContext(textarea); + + if (type && context) { + results.push({ element: textarea, type, context }); + } + } + + return results; + } + + extractContext(textarea: HTMLTextAreaElement): RedditContext | null { + const pathname = window.location.pathname; + + // Parse Reddit URL structure: /r/subreddit/comments/postid/title/ + const postMatch = pathname.match(/^\/r\/([^\/]+)\/comments\/([^\/]+)/); + const submitMatch = pathname.match(/^\/r\/([^\/]+)\/submit/); + const subredditMatch = pathname.match(/^\/r\/([^\/]+)/); + + let subreddit: string; + let postId: string | undefined; + + if (postMatch) { + [, subreddit, postId] = postMatch; + } else if (submitMatch) { + [, subreddit] = submitMatch; + } else if (subredditMatch) { + [, subreddit] = subredditMatch; + } else { + return null; + } + + // Generate unique key + let unique_key = `reddit:${subreddit}`; + if (postId) { + unique_key += `:${postId}`; + } else { + unique_key += ':new'; + } + + // Check if editing existing comment + const commentId = this.getCommentId(textarea); + if (commentId) { + unique_key += `:edit:${commentId}`; + } + + return { + unique_key, + subreddit, + postId, + commentId + }; + } + + determineType(textarea: HTMLTextAreaElement): CommentType | null { + const pathname = window.location.pathname; + + // New post submission + if (pathname.includes('/submit')) { + return 'REDDIT_POST_NEW'; + } + + // Check if we're on a post page + if (pathname.match(/\/r\/[^\/]+\/comments\/[^\/]+/)) { + const isEditingComment = this.getCommentId(textarea) !== null; + return isEditingComment ? 'REDDIT_COMMENT_EDIT' : 'REDDIT_COMMENT_NEW'; + } + + return null; + } + + generateDisplayTitle(context: RedditContext): string { + const { subreddit, postId, commentId } = context; + + if (commentId) { + return `Edit comment in r/${subreddit}`; + } + + if (postId) { + return `Comment in r/${subreddit}`; + } + + return `New post in r/${subreddit}`; + } + + generateIcon(type: CommentType): string { + switch (type) { + case 'REDDIT_POST_NEW': + return '📝'; // Post icon + case 'REDDIT_COMMENT_NEW': + return '💬'; // Comment icon + case 'REDDIT_COMMENT_EDIT': + return '✏️'; // Edit icon + default: + return '🔵'; // Reddit icon + } + } + + buildUrl(context: RedditContext, withDraft?: boolean): string { + const baseUrl = `https://reddit.com/r/${context.subreddit}`; + + if (context.postId) { + return `${baseUrl}/comments/${context.postId}/${context.commentId ? `#${context.commentId}` : ''}`; + } + + return baseUrl; + } + + private getCommentId(textarea: HTMLTextAreaElement): string | null { + // Look for edit comment form indicators + const commentForm = textarea.closest('[data-comment-id]'); + if (commentForm) { + return commentForm.getAttribute('data-comment-id'); + } + + // Reddit uses different class names, check for common edit form patterns + const editForm = textarea.closest('.edit-usertext') || + textarea.closest('[data-type="comment"]'); + if (editForm) { + const id = editForm.getAttribute('data-fullname') || + editForm.getAttribute('data-comment-id'); + return id; + } + + return null; + } +} \ No newline at end of file From 038710ca6e8adcbcac1b0f5c02dcaee965f4a9f5 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 16:08:17 -0700 Subject: [PATCH 04/54] Idiomatic typescript. --- .../src/datamodel/handler-registry.ts | 6 ++-- .../src/datamodel/textarea-handler.ts | 33 ++++--------------- .../src/handlers/github-handler.ts | 26 ++++++++++----- .../src/handlers/reddit-handler.ts | 27 +++++++++------ 4 files changed, 44 insertions(+), 48 deletions(-) diff --git a/browser-extension/src/datamodel/handler-registry.ts b/browser-extension/src/datamodel/handler-registry.ts index 8d83c0a..13bf6fa 100644 --- a/browser-extension/src/datamodel/handler-registry.ts +++ b/browser-extension/src/datamodel/handler-registry.ts @@ -1,4 +1,4 @@ -import { CommentType, CommentContext, TextareaHandler, TextareaInfo } from './textarea-handler'; +import { CommentContext, TextareaHandler, TextareaInfo } from './textarea-handler'; import { GitHubHandler } from '../handlers/github-handler'; import { RedditHandler } from '../handlers/reddit-handler'; @@ -15,7 +15,7 @@ export class HandlerRegistry { this.handlers.add(handler); } - getHandlerForType(type: CommentType): TextareaHandler | null { + getHandlerForType(type: string): TextareaHandler | null { for (const handler of this.handlers) { if (handler.forCommentTypes().includes(type)) { return handler; @@ -43,7 +43,7 @@ export class HandlerRegistry { return Array.from(this.handlers); } - getCommentTypesForHandler(handler: TextareaHandler): CommentType[] { + getCommentTypesForHandler(handler: TextareaHandler): string[] { return handler.forCommentTypes(); } } \ No newline at end of file diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts index 02e04af..48fa254 100644 --- a/browser-extension/src/datamodel/textarea-handler.ts +++ b/browser-extension/src/datamodel/textarea-handler.ts @@ -1,22 +1,3 @@ -export type CommentType = - | 'GH_ISSUE_NEW' - | 'GH_PR_NEW' - | 'GH_ISSUE_ADD_COMMENT' - | 'GH_ISSUE_EDIT_COMMENT' - | 'GH_PR_ADD_COMMENT' - | 'GH_PR_EDIT_COMMENT' - | 'GH_PR_CODE_COMMENT' - | 'REDDIT_POST_NEW' - | 'REDDIT_COMMENT_NEW' - | 'REDDIT_COMMENT_EDIT' - | 'GL_ISSUE_NEW' - | 'GL_MR_NEW' - | 'GL_ISSUE_ADD_COMMENT' - | 'GL_MR_ADD_COMMENT' - | 'BB_ISSUE_NEW' - | 'BB_PR_NEW' - | 'BB_ISSUE_ADD_COMMENT' - | 'BB_PR_ADD_COMMENT'; export interface CommentContext { unique_key: string; @@ -24,13 +5,13 @@ export interface CommentContext { export interface TextareaInfo { element: HTMLTextAreaElement; - type: CommentType; + type: string; context: T; } export interface TextareaHandler { // Handler metadata - forCommentTypes(): CommentType[]; + forCommentTypes(): string[]; // Content script functionality identify(): TextareaInfo[]; @@ -40,11 +21,11 @@ export interface TextareaHandler { // Context extraction extractContext(textarea: HTMLTextAreaElement): T | null; - determineType(textarea: HTMLTextAreaElement): CommentType | null; + determineType(textarea: HTMLTextAreaElement): string | null; // Popup functionality helpers generateDisplayTitle(context: T): string; - generateIcon(type: CommentType): string; + generateIcon(type: string): string; buildUrl(context: T, withDraft?: boolean): string; } @@ -55,12 +36,12 @@ export abstract class BaseTextareaHandler[]; abstract extractContext(textarea: HTMLTextAreaElement): T | null; - abstract determineType(textarea: HTMLTextAreaElement): CommentType | null; + abstract determineType(textarea: HTMLTextAreaElement): string | null; abstract generateDisplayTitle(context: T): string; - abstract generateIcon(type: CommentType): string; + abstract generateIcon(type: string): string; abstract buildUrl(context: T, withDraft?: boolean): string; readContent(textarea: HTMLTextAreaElement): string { diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/handlers/github-handler.ts index 55775f8..08ed247 100644 --- a/browser-extension/src/handlers/github-handler.ts +++ b/browser-extension/src/handlers/github-handler.ts @@ -1,10 +1,19 @@ -import { CommentType, CommentContext, BaseTextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; +import { CommentContext, BaseTextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; + +export type GitHubCommentType = + | 'GH_ISSUE_NEW' + | 'GH_PR_NEW' + | 'GH_ISSUE_ADD_COMMENT' + | 'GH_ISSUE_EDIT_COMMENT' + | 'GH_PR_ADD_COMMENT' + | 'GH_PR_EDIT_COMMENT' + | 'GH_PR_CODE_COMMENT'; export interface GitHubContext extends CommentContext { domain: string; slug: string; // owner/repo - number?: number; // issue/PR number - commentId?: string; // for editing existing comments + number?: number | undefined; // issue/PR number + commentId?: string | undefined; // for editing existing comments } export class GitHubHandler extends BaseTextareaHandler { @@ -12,7 +21,7 @@ export class GitHubHandler extends BaseTextareaHandler { super('github.com'); } - forCommentTypes(): CommentType[] { + forCommentTypes(): string[] { return [ 'GH_ISSUE_NEW', 'GH_PR_NEW', @@ -41,7 +50,6 @@ export class GitHubHandler extends BaseTextareaHandler { } extractContext(textarea: HTMLTextAreaElement): GitHubContext | null { - const url = window.location.href; const pathname = window.location.pathname; // Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456 @@ -71,11 +79,11 @@ export class GitHubHandler extends BaseTextareaHandler { domain: window.location.hostname, slug, number, - commentId + commentId: commentId || undefined }; } - determineType(textarea: HTMLTextAreaElement): CommentType | null { + determineType(textarea: HTMLTextAreaElement): GitHubCommentType | null { const pathname = window.location.pathname; // New issue @@ -124,7 +132,7 @@ export class GitHubHandler extends BaseTextareaHandler { return `New ${window.location.pathname.includes('/issues/') ? 'issue' : 'PR'} in ${slug}`; } - generateIcon(type: CommentType): string { + generateIcon(type: string): string { switch (type) { case 'GH_ISSUE_NEW': case 'GH_ISSUE_ADD_COMMENT': @@ -141,7 +149,7 @@ export class GitHubHandler extends BaseTextareaHandler { } } - buildUrl(context: GitHubContext, withDraft?: boolean): string { + buildUrl(context: GitHubContext): string { const baseUrl = `https://${context.domain}/${context.slug}`; if (context.number) { diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/handlers/reddit-handler.ts index 465cb38..53d1c85 100644 --- a/browser-extension/src/handlers/reddit-handler.ts +++ b/browser-extension/src/handlers/reddit-handler.ts @@ -1,9 +1,14 @@ -import { CommentType, CommentContext, BaseTextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; +import { CommentContext, BaseTextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; + +export type RedditCommentType = + | 'REDDIT_POST_NEW' + | 'REDDIT_COMMENT_NEW' + | 'REDDIT_COMMENT_EDIT'; export interface RedditContext extends CommentContext { subreddit: string; - postId?: string; - commentId?: string; // for editing existing comments + postId?: string | undefined; + commentId?: string | undefined; // for editing existing comments } export class RedditHandler extends BaseTextareaHandler { @@ -11,7 +16,7 @@ export class RedditHandler extends BaseTextareaHandler { super('reddit.com'); } - forCommentTypes(): CommentType[] { + forCommentTypes(): string[] { return [ 'REDDIT_POST_NEW', 'REDDIT_COMMENT_NEW', @@ -43,7 +48,7 @@ export class RedditHandler extends BaseTextareaHandler { const submitMatch = pathname.match(/^\/r\/([^\/]+)\/submit/); const subredditMatch = pathname.match(/^\/r\/([^\/]+)/); - let subreddit: string; + let subreddit: string | undefined; let postId: string | undefined; if (postMatch) { @@ -52,7 +57,9 @@ export class RedditHandler extends BaseTextareaHandler { [, subreddit] = submitMatch; } else if (subredditMatch) { [, subreddit] = subredditMatch; - } else { + } + + if (!subreddit) { return null; } @@ -74,11 +81,11 @@ export class RedditHandler extends BaseTextareaHandler { unique_key, subreddit, postId, - commentId + commentId: commentId || undefined }; } - determineType(textarea: HTMLTextAreaElement): CommentType | null { + determineType(textarea: HTMLTextAreaElement): RedditCommentType | null { const pathname = window.location.pathname; // New post submission @@ -109,7 +116,7 @@ export class RedditHandler extends BaseTextareaHandler { return `New post in r/${subreddit}`; } - generateIcon(type: CommentType): string { + generateIcon(type: string): string { switch (type) { case 'REDDIT_POST_NEW': return '📝'; // Post icon @@ -122,7 +129,7 @@ export class RedditHandler extends BaseTextareaHandler { } } - buildUrl(context: RedditContext, withDraft?: boolean): string { + buildUrl(context: RedditContext): string { const baseUrl = `https://reddit.com/r/${context.subreddit}`; if (context.postId) { From f66e7a9a43be7dfd2611c1f66614307ca7b9888d Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 16:43:31 -0700 Subject: [PATCH 05/54] `identifyContextOf` --- .../src/datamodel/handler-registry.ts | 14 ++++++++++++++ .../src/datamodel/textarea-handler.ts | 3 +++ browser-extension/src/entrypoints/content.ts | 10 ++++++---- browser-extension/src/handlers/github-handler.ts | 16 ++++++++++++++++ browser-extension/src/handlers/reddit-handler.ts | 16 ++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/browser-extension/src/datamodel/handler-registry.ts b/browser-extension/src/datamodel/handler-registry.ts index 13bf6fa..81e2ba2 100644 --- a/browser-extension/src/datamodel/handler-registry.ts +++ b/browser-extension/src/datamodel/handler-registry.ts @@ -24,6 +24,20 @@ export class HandlerRegistry { return null; } + identifyTextarea(textarea: HTMLTextAreaElement): TextareaInfo | null { + for (const handler of this.handlers) { + try { + const result = handler.identifyContextOf(textarea); + if (result) { + return result; + } + } catch (error) { + console.warn('Handler failed to identify textarea:', error); + } + } + return null; + } + identifyAll(): TextareaInfo[] { const allTextareas: TextareaInfo[] = []; diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts index 48fa254..8313296 100644 --- a/browser-extension/src/datamodel/textarea-handler.ts +++ b/browser-extension/src/datamodel/textarea-handler.ts @@ -12,6 +12,8 @@ export interface TextareaInfo { export interface TextareaHandler { // Handler metadata forCommentTypes(): string[]; + // whenever a new `textarea` is added to any webpage, this method is called to try to find a handler for it + identifyContextOf(textarea: HTMLTextAreaElement): TextareaInfo | null; // Content script functionality identify(): TextareaInfo[]; @@ -37,6 +39,7 @@ export abstract class BaseTextareaHandler | null; abstract identify(): TextareaInfo[]; abstract extractContext(textarea: HTMLTextAreaElement): T | null; abstract determineType(textarea: HTMLTextAreaElement): string | null; diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index ff85d70..59b749c 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -48,11 +48,13 @@ function initializeMaybe(textarea: HTMLTextAreaElement) { injectStyles() textarea.classList.add(CONFIG.ADDED_OVERTYPE_CLASS) - // Use registry to identify and handle this textarea - const textareaInfos = registry.identifyAll().filter(info => info.element === textarea) - for (const info of textareaInfos) { - logger.debug('Identified textarea:', info.type, info.context.unique_key) + // Use registry to identify and handle this specific textarea + const textareaInfo = registry.identifyTextarea(textarea) + if (textareaInfo) { + logger.debug('Identified textarea:', textareaInfo.type, textareaInfo.context.unique_key) // TODO: Set up textarea monitoring and draft saving + } else { + logger.debug('No handler found for textarea') } } else { logger.debug('already activated textarea {}', textarea) diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/handlers/github-handler.ts index 08ed247..4929409 100644 --- a/browser-extension/src/handlers/github-handler.ts +++ b/browser-extension/src/handlers/github-handler.ts @@ -33,6 +33,22 @@ export class GitHubHandler extends BaseTextareaHandler { ]; } + identifyContextOf(textarea: HTMLTextAreaElement): TextareaInfo | null { + // Only handle GitHub domains + if (!window.location.hostname.includes('github')) { + return null; + } + + const type = this.determineType(textarea); + const context = this.extractContext(textarea); + + if (type && context) { + return { element: textarea, type, context }; + } + + return null; + } + identify(): TextareaInfo[] { const textareas = document.querySelectorAll('textarea'); const results: TextareaInfo[] = []; diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/handlers/reddit-handler.ts index 53d1c85..91a5cc5 100644 --- a/browser-extension/src/handlers/reddit-handler.ts +++ b/browser-extension/src/handlers/reddit-handler.ts @@ -24,6 +24,22 @@ export class RedditHandler extends BaseTextareaHandler { ]; } + identifyContextOf(textarea: HTMLTextAreaElement): TextareaInfo | null { + // Only handle Reddit domains + if (!window.location.hostname.includes('reddit')) { + return null; + } + + const type = this.determineType(textarea); + const context = this.extractContext(textarea); + + if (type && context) { + return { element: textarea, type, context }; + } + + return null; + } + identify(): TextareaInfo[] { const textareas = document.querySelectorAll('textarea'); const results: TextareaInfo[] = []; From 0161e0b748edc3d376cd29c7e6d9355cb087c136 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 16:46:58 -0700 Subject: [PATCH 06/54] Remove unnecessary methods. --- .../src/datamodel/handler-registry.ts | 14 ---------- .../src/datamodel/textarea-handler.ts | 28 +------------------ .../src/handlers/github-handler.ts | 15 ---------- .../src/handlers/reddit-handler.ts | 15 ---------- 4 files changed, 1 insertion(+), 71 deletions(-) diff --git a/browser-extension/src/datamodel/handler-registry.ts b/browser-extension/src/datamodel/handler-registry.ts index 81e2ba2..09f70a1 100644 --- a/browser-extension/src/datamodel/handler-registry.ts +++ b/browser-extension/src/datamodel/handler-registry.ts @@ -38,20 +38,6 @@ export class HandlerRegistry { return null; } - identifyAll(): TextareaInfo[] { - const allTextareas: TextareaInfo[] = []; - - for (const handler of this.handlers) { - try { - const textareas = handler.identify(); - allTextareas.push(...textareas); - } catch (error) { - console.warn('Handler failed to identify textareas:', error); - } - } - - return allTextareas; - } getAllHandlers(): TextareaHandler[] { return Array.from(this.handlers); diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts index 8313296..2bd091e 100644 --- a/browser-extension/src/datamodel/textarea-handler.ts +++ b/browser-extension/src/datamodel/textarea-handler.ts @@ -14,13 +14,7 @@ export interface TextareaHandler { forCommentTypes(): string[]; // whenever a new `textarea` is added to any webpage, this method is called to try to find a handler for it identifyContextOf(textarea: HTMLTextAreaElement): TextareaInfo | null; - - // Content script functionality - identify(): TextareaInfo[]; - readContent(textarea: HTMLTextAreaElement): string; - setContent(textarea: HTMLTextAreaElement, content: string): void; - onSubmit(textarea: HTMLTextAreaElement, callback: (success: boolean) => void): void; - + // Context extraction extractContext(textarea: HTMLTextAreaElement): T | null; determineType(textarea: HTMLTextAreaElement): string | null; @@ -40,29 +34,9 @@ export abstract class BaseTextareaHandler | null; - abstract identify(): TextareaInfo[]; abstract extractContext(textarea: HTMLTextAreaElement): T | null; abstract determineType(textarea: HTMLTextAreaElement): string | null; abstract generateDisplayTitle(context: T): string; abstract generateIcon(type: string): string; abstract buildUrl(context: T, withDraft?: boolean): string; - - readContent(textarea: HTMLTextAreaElement): string { - return textarea.value; - } - - setContent(textarea: HTMLTextAreaElement, content: string): void { - textarea.value = content; - textarea.dispatchEvent(new Event('input', { bubbles: true })); - textarea.dispatchEvent(new Event('change', { bubbles: true })); - } - - onSubmit(textarea: HTMLTextAreaElement, callback: (success: boolean) => void): void { - const form = textarea.closest('form'); - if (form) { - form.addEventListener('submit', () => { - setTimeout(() => callback(true), 100); - }, { once: true }); - } - } } \ No newline at end of file diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/handlers/github-handler.ts index 4929409..7c8c638 100644 --- a/browser-extension/src/handlers/github-handler.ts +++ b/browser-extension/src/handlers/github-handler.ts @@ -49,21 +49,6 @@ export class GitHubHandler extends BaseTextareaHandler { return null; } - identify(): TextareaInfo[] { - const textareas = document.querySelectorAll('textarea'); - const results: TextareaInfo[] = []; - - for (const textarea of textareas) { - const type = this.determineType(textarea); - const context = this.extractContext(textarea); - - if (type && context) { - results.push({ element: textarea, type, context }); - } - } - - return results; - } extractContext(textarea: HTMLTextAreaElement): GitHubContext | null { const pathname = window.location.pathname; diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/handlers/reddit-handler.ts index 91a5cc5..dc60b9b 100644 --- a/browser-extension/src/handlers/reddit-handler.ts +++ b/browser-extension/src/handlers/reddit-handler.ts @@ -40,21 +40,6 @@ export class RedditHandler extends BaseTextareaHandler { return null; } - identify(): TextareaInfo[] { - const textareas = document.querySelectorAll('textarea'); - const results: TextareaInfo[] = []; - - for (const textarea of textareas) { - const type = this.determineType(textarea); - const context = this.extractContext(textarea); - - if (type && context) { - results.push({ element: textarea, type, context }); - } - } - - return results; - } extractContext(textarea: HTMLTextAreaElement): RedditContext | null { const pathname = window.location.pathname; From 4ca0261a3c186dbc15adbbb64be71bf67ca9da24 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 16:48:04 -0700 Subject: [PATCH 07/54] Remove unnecessary base class. --- .../src/datamodel/textarea-handler.ts | 15 --------------- browser-extension/src/handlers/github-handler.ts | 7 ++----- browser-extension/src/handlers/reddit-handler.ts | 7 ++----- 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts index 2bd091e..2730d0f 100644 --- a/browser-extension/src/datamodel/textarea-handler.ts +++ b/browser-extension/src/datamodel/textarea-handler.ts @@ -25,18 +25,3 @@ export interface TextareaHandler { buildUrl(context: T, withDraft?: boolean): string; } -export abstract class BaseTextareaHandler implements TextareaHandler { - protected domain: string; - - constructor(domain: string) { - this.domain = domain; - } - - abstract forCommentTypes(): string[]; - abstract identifyContextOf(textarea: HTMLTextAreaElement): TextareaInfo | null; - abstract extractContext(textarea: HTMLTextAreaElement): T | null; - abstract determineType(textarea: HTMLTextAreaElement): string | null; - abstract generateDisplayTitle(context: T): string; - abstract generateIcon(type: string): string; - abstract buildUrl(context: T, withDraft?: boolean): string; -} \ No newline at end of file diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/handlers/github-handler.ts index 7c8c638..e531880 100644 --- a/browser-extension/src/handlers/github-handler.ts +++ b/browser-extension/src/handlers/github-handler.ts @@ -1,4 +1,4 @@ -import { CommentContext, BaseTextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; +import { CommentContext, TextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; export type GitHubCommentType = | 'GH_ISSUE_NEW' @@ -16,10 +16,7 @@ export interface GitHubContext extends CommentContext { commentId?: string | undefined; // for editing existing comments } -export class GitHubHandler extends BaseTextareaHandler { - constructor() { - super('github.com'); - } +export class GitHubHandler implements TextareaHandler { forCommentTypes(): string[] { return [ diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/handlers/reddit-handler.ts index dc60b9b..1bced14 100644 --- a/browser-extension/src/handlers/reddit-handler.ts +++ b/browser-extension/src/handlers/reddit-handler.ts @@ -1,4 +1,4 @@ -import { CommentContext, BaseTextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; +import { CommentContext, TextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; export type RedditCommentType = | 'REDDIT_POST_NEW' @@ -11,10 +11,7 @@ export interface RedditContext extends CommentContext { commentId?: string | undefined; // for editing existing comments } -export class RedditHandler extends BaseTextareaHandler { - constructor() { - super('reddit.com'); - } +export class RedditHandler implements TextareaHandler { forCommentTypes(): string[] { return [ From 88564d94c4a0472afb718e1fe555d265ed4a391c Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 16:56:02 -0700 Subject: [PATCH 08/54] Move `type` from `TextareaInfo` into `CommentContext`. --- browser-extension/src/datamodel/textarea-handler.ts | 8 ++------ browser-extension/src/entrypoints/content.ts | 2 +- browser-extension/src/handlers/github-handler.ts | 11 ++++++----- browser-extension/src/handlers/reddit-handler.ts | 11 ++++++----- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts index 2730d0f..87af187 100644 --- a/browser-extension/src/datamodel/textarea-handler.ts +++ b/browser-extension/src/datamodel/textarea-handler.ts @@ -1,11 +1,11 @@ export interface CommentContext { unique_key: string; + type: string; } export interface TextareaInfo { element: HTMLTextAreaElement; - type: string; context: T; } @@ -14,14 +14,10 @@ export interface TextareaHandler { forCommentTypes(): string[]; // whenever a new `textarea` is added to any webpage, this method is called to try to find a handler for it identifyContextOf(textarea: HTMLTextAreaElement): TextareaInfo | null; - - // Context extraction - extractContext(textarea: HTMLTextAreaElement): T | null; - determineType(textarea: HTMLTextAreaElement): string | null; // Popup functionality helpers generateDisplayTitle(context: T): string; - generateIcon(type: string): string; + generateIcon(context: T): string; buildUrl(context: T, withDraft?: boolean): string; } diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 59b749c..27b804b 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -51,7 +51,7 @@ function initializeMaybe(textarea: HTMLTextAreaElement) { // Use registry to identify and handle this specific textarea const textareaInfo = registry.identifyTextarea(textarea) if (textareaInfo) { - logger.debug('Identified textarea:', textareaInfo.type, textareaInfo.context.unique_key) + logger.debug('Identified textarea:', textareaInfo.context.type, textareaInfo.context.unique_key) // TODO: Set up textarea monitoring and draft saving } else { logger.debug('No handler found for textarea') diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/handlers/github-handler.ts index e531880..4bb0577 100644 --- a/browser-extension/src/handlers/github-handler.ts +++ b/browser-extension/src/handlers/github-handler.ts @@ -40,14 +40,14 @@ export class GitHubHandler implements TextareaHandler { const context = this.extractContext(textarea); if (type && context) { - return { element: textarea, type, context }; + return { element: textarea, context: { ...context, type } }; } return null; } - extractContext(textarea: HTMLTextAreaElement): GitHubContext | null { + private extractContext(textarea: HTMLTextAreaElement): GitHubContext | null { const pathname = window.location.pathname; // Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456 @@ -74,6 +74,7 @@ export class GitHubHandler implements TextareaHandler { return { unique_key, + type: '', // Will be set by caller domain: window.location.hostname, slug, number, @@ -81,7 +82,7 @@ export class GitHubHandler implements TextareaHandler { }; } - determineType(textarea: HTMLTextAreaElement): GitHubCommentType | null { + private determineType(textarea: HTMLTextAreaElement): GitHubCommentType | null { const pathname = window.location.pathname; // New issue @@ -130,8 +131,8 @@ export class GitHubHandler implements TextareaHandler { return `New ${window.location.pathname.includes('/issues/') ? 'issue' : 'PR'} in ${slug}`; } - generateIcon(type: string): string { - switch (type) { + generateIcon(context: GitHubContext): string { + switch (context.type) { case 'GH_ISSUE_NEW': case 'GH_ISSUE_ADD_COMMENT': case 'GH_ISSUE_EDIT_COMMENT': diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/handlers/reddit-handler.ts index 1bced14..ea45718 100644 --- a/browser-extension/src/handlers/reddit-handler.ts +++ b/browser-extension/src/handlers/reddit-handler.ts @@ -31,14 +31,14 @@ export class RedditHandler implements TextareaHandler { const context = this.extractContext(textarea); if (type && context) { - return { element: textarea, type, context }; + return { element: textarea, context: { ...context, type } }; } return null; } - extractContext(textarea: HTMLTextAreaElement): RedditContext | null { + private extractContext(textarea: HTMLTextAreaElement): RedditContext | null { const pathname = window.location.pathname; // Parse Reddit URL structure: /r/subreddit/comments/postid/title/ @@ -77,13 +77,14 @@ export class RedditHandler implements TextareaHandler { return { unique_key, + type: '', // Will be set by caller subreddit, postId, commentId: commentId || undefined }; } - determineType(textarea: HTMLTextAreaElement): RedditCommentType | null { + private determineType(textarea: HTMLTextAreaElement): RedditCommentType | null { const pathname = window.location.pathname; // New post submission @@ -114,8 +115,8 @@ export class RedditHandler implements TextareaHandler { return `New post in r/${subreddit}`; } - generateIcon(type: string): string { - switch (type) { + generateIcon(context: RedditContext): string { + switch (context.type) { case 'REDDIT_POST_NEW': return '📝'; // Post icon case 'REDDIT_COMMENT_NEW': From d35d4ae342d35a09a793a1eb7017ecc9646462c1 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 17:02:06 -0700 Subject: [PATCH 09/54] Fixup unnecessary splitting. --- .../src/handlers/github-handler.ts | 94 ++++++++----------- .../src/handlers/reddit-handler.ts | 53 +++++------ 2 files changed, 62 insertions(+), 85 deletions(-) diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/handlers/github-handler.ts index 4bb0577..d0ca1db 100644 --- a/browser-extension/src/handlers/github-handler.ts +++ b/browser-extension/src/handlers/github-handler.ts @@ -36,85 +36,73 @@ export class GitHubHandler implements TextareaHandler { return null; } - const type = this.determineType(textarea); - const context = this.extractContext(textarea); - - if (type && context) { - return { element: textarea, context: { ...context, type } }; - } - - return null; - } - - - private extractContext(textarea: HTMLTextAreaElement): GitHubContext | null { const pathname = window.location.pathname; // Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456 const match = pathname.match(/^\/([^\/]+)\/([^\/]+)(?:\/(issues|pull)\/(\d+))?/); if (!match) return null; - const [, owner, repo, type, numberStr] = match; + const [, owner, repo, urlType, numberStr] = match; const slug = `${owner}/${repo}`; const number = numberStr ? parseInt(numberStr, 10) : undefined; + // Check if editing existing comment + const commentId = this.getCommentId(textarea); + + // Determine comment type + let type: GitHubCommentType; + + // New issue + if (pathname.includes('/issues/new')) { + type = 'GH_ISSUE_NEW'; + } + // New PR + else if (pathname.includes('/compare/') || pathname.endsWith('/compare')) { + type = 'GH_PR_NEW'; + } + // Existing issue or PR page + else if (urlType && number) { + const isEditingComment = commentId !== null; + + if (urlType === 'issues') { + type = isEditingComment ? 'GH_ISSUE_EDIT_COMMENT' : 'GH_ISSUE_ADD_COMMENT'; + } else { + // Check if it's a code comment (in Files Changed tab) + const isCodeComment = textarea.closest('.js-inline-comment-form') !== null || + textarea.closest('[data-path]') !== null; + + if (isCodeComment) { + type = 'GH_PR_CODE_COMMENT'; + } else { + type = isEditingComment ? 'GH_PR_EDIT_COMMENT' : 'GH_PR_ADD_COMMENT'; + } + } + } else { + return null; + } + // Generate unique key based on context let unique_key = `github:${slug}`; if (number) { - unique_key += `:${type}:${number}`; + unique_key += `:${urlType}:${number}`; } else { unique_key += ':new'; } - - // Check if editing existing comment - const commentId = this.getCommentId(textarea); + if (commentId) { unique_key += `:edit:${commentId}`; } - return { + const context: GitHubContext = { unique_key, - type: '', // Will be set by caller + type, domain: window.location.hostname, slug, number, commentId: commentId || undefined }; - } - private determineType(textarea: HTMLTextAreaElement): GitHubCommentType | null { - const pathname = window.location.pathname; - - // New issue - if (pathname.includes('/issues/new')) { - return 'GH_ISSUE_NEW'; - } - - // New PR - if (pathname.includes('/compare/') || pathname.endsWith('/compare')) { - return 'GH_PR_NEW'; - } - - // Check if we're on an issue or PR page - const match = pathname.match(/\/(issues|pull)\/(\d+)/); - if (!match) return null; - - const [, type] = match; - const isEditingComment = this.getCommentId(textarea) !== null; - - if (type === 'issues') { - return isEditingComment ? 'GH_ISSUE_EDIT_COMMENT' : 'GH_ISSUE_ADD_COMMENT'; - } else { - // Check if it's a code comment (in Files Changed tab) - const isCodeComment = textarea.closest('.js-inline-comment-form') !== null || - textarea.closest('[data-path]') !== null; - - if (isCodeComment) { - return 'GH_PR_CODE_COMMENT'; - } - - return isEditingComment ? 'GH_PR_EDIT_COMMENT' : 'GH_PR_ADD_COMMENT'; - } + return { element: textarea, context }; } generateDisplayTitle(context: GitHubContext): string { diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/handlers/reddit-handler.ts index ea45718..569f144 100644 --- a/browser-extension/src/handlers/reddit-handler.ts +++ b/browser-extension/src/handlers/reddit-handler.ts @@ -27,18 +27,6 @@ export class RedditHandler implements TextareaHandler { return null; } - const type = this.determineType(textarea); - const context = this.extractContext(textarea); - - if (type && context) { - return { element: textarea, context: { ...context, type } }; - } - - return null; - } - - - private extractContext(textarea: HTMLTextAreaElement): RedditContext | null { const pathname = window.location.pathname; // Parse Reddit URL structure: /r/subreddit/comments/postid/title/ @@ -61,6 +49,24 @@ export class RedditHandler implements TextareaHandler { return null; } + // Check if editing existing comment + const commentId = this.getCommentId(textarea); + + // Determine comment type + let type: RedditCommentType; + + // New post submission + if (pathname.includes('/submit')) { + type = 'REDDIT_POST_NEW'; + } + // Check if we're on a post page + else if (pathname.match(/\/r\/[^\/]+\/comments\/[^\/]+/)) { + const isEditingComment = commentId !== null; + type = isEditingComment ? 'REDDIT_COMMENT_EDIT' : 'REDDIT_COMMENT_NEW'; + } else { + return null; + } + // Generate unique key let unique_key = `reddit:${subreddit}`; if (postId) { @@ -69,36 +75,19 @@ export class RedditHandler implements TextareaHandler { unique_key += ':new'; } - // Check if editing existing comment - const commentId = this.getCommentId(textarea); if (commentId) { unique_key += `:edit:${commentId}`; } - return { + const context: RedditContext = { unique_key, - type: '', // Will be set by caller + type, subreddit, postId, commentId: commentId || undefined }; - } - private determineType(textarea: HTMLTextAreaElement): RedditCommentType | null { - const pathname = window.location.pathname; - - // New post submission - if (pathname.includes('/submit')) { - return 'REDDIT_POST_NEW'; - } - - // Check if we're on a post page - if (pathname.match(/\/r\/[^\/]+\/comments\/[^\/]+/)) { - const isEditingComment = this.getCommentId(textarea) !== null; - return isEditingComment ? 'REDDIT_COMMENT_EDIT' : 'REDDIT_COMMENT_NEW'; - } - - return null; + return { element: textarea, context }; } generateDisplayTitle(context: RedditContext): string { From c53b60d9d433f146269b093f7094a5e2d5611a0f Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 17:04:42 -0700 Subject: [PATCH 10/54] More refactor cleanup. --- browser-extension/src/datamodel/handler-registry.ts | 6 +++--- browser-extension/src/datamodel/textarea-handler.ts | 2 +- browser-extension/src/handlers/github-handler.ts | 4 ++-- browser-extension/src/handlers/reddit-handler.ts | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/browser-extension/src/datamodel/handler-registry.ts b/browser-extension/src/datamodel/handler-registry.ts index 09f70a1..89074dd 100644 --- a/browser-extension/src/datamodel/handler-registry.ts +++ b/browser-extension/src/datamodel/handler-registry.ts @@ -27,9 +27,9 @@ export class HandlerRegistry { identifyTextarea(textarea: HTMLTextAreaElement): TextareaInfo | null { for (const handler of this.handlers) { try { - const result = handler.identifyContextOf(textarea); - if (result) { - return result; + const context = handler.identifyContextOf(textarea); + if (context) { + return { element: textarea, context }; } } catch (error) { console.warn('Handler failed to identify textarea:', error); diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts index 87af187..ccec6ca 100644 --- a/browser-extension/src/datamodel/textarea-handler.ts +++ b/browser-extension/src/datamodel/textarea-handler.ts @@ -13,7 +13,7 @@ export interface TextareaHandler { // Handler metadata forCommentTypes(): string[]; // whenever a new `textarea` is added to any webpage, this method is called to try to find a handler for it - identifyContextOf(textarea: HTMLTextAreaElement): TextareaInfo | null; + identifyContextOf(textarea: HTMLTextAreaElement): T | null; // Popup functionality helpers generateDisplayTitle(context: T): string; diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/handlers/github-handler.ts index d0ca1db..bfd076b 100644 --- a/browser-extension/src/handlers/github-handler.ts +++ b/browser-extension/src/handlers/github-handler.ts @@ -30,7 +30,7 @@ export class GitHubHandler implements TextareaHandler { ]; } - identifyContextOf(textarea: HTMLTextAreaElement): TextareaInfo | null { + identifyContextOf(textarea: HTMLTextAreaElement): GitHubContext | null { // Only handle GitHub domains if (!window.location.hostname.includes('github')) { return null; @@ -102,7 +102,7 @@ export class GitHubHandler implements TextareaHandler { commentId: commentId || undefined }; - return { element: textarea, context }; + return context; } generateDisplayTitle(context: GitHubContext): string { diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/handlers/reddit-handler.ts index 569f144..06f63b0 100644 --- a/browser-extension/src/handlers/reddit-handler.ts +++ b/browser-extension/src/handlers/reddit-handler.ts @@ -21,7 +21,7 @@ export class RedditHandler implements TextareaHandler { ]; } - identifyContextOf(textarea: HTMLTextAreaElement): TextareaInfo | null { + identifyContextOf(textarea: HTMLTextAreaElement): RedditContext | null { // Only handle Reddit domains if (!window.location.hostname.includes('reddit')) { return null; @@ -87,7 +87,7 @@ export class RedditHandler implements TextareaHandler { commentId: commentId || undefined }; - return { element: textarea, context }; + return context; } generateDisplayTitle(context: RedditContext): string { From 58ed64d1211f3fd8a613392c73e4b440f65ba468 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 17:07:31 -0700 Subject: [PATCH 11/54] Type narrowing. --- browser-extension/src/handlers/github-handler.ts | 1 + browser-extension/src/handlers/reddit-handler.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/handlers/github-handler.ts index bfd076b..5226e06 100644 --- a/browser-extension/src/handlers/github-handler.ts +++ b/browser-extension/src/handlers/github-handler.ts @@ -10,6 +10,7 @@ export type GitHubCommentType = | 'GH_PR_CODE_COMMENT'; export interface GitHubContext extends CommentContext { + type: GitHubCommentType; // Override to narrow from string to specific union domain: string; slug: string; // owner/repo number?: number | undefined; // issue/PR number diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/handlers/reddit-handler.ts index 06f63b0..1a9e70a 100644 --- a/browser-extension/src/handlers/reddit-handler.ts +++ b/browser-extension/src/handlers/reddit-handler.ts @@ -6,6 +6,7 @@ export type RedditCommentType = | 'REDDIT_COMMENT_EDIT'; export interface RedditContext extends CommentContext { + type: RedditCommentType; // Override to narrow from string to specific union subreddit: string; postId?: string | undefined; commentId?: string | undefined; // for editing existing comments From 5a5d3a86c82524a801be52e2936083c29c6c2c2d Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 17:08:47 -0700 Subject: [PATCH 12/54] Wow it all compiles (!!!) --- browser-extension/src/handlers/github-handler.ts | 2 +- browser-extension/src/handlers/reddit-handler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/handlers/github-handler.ts index 5226e06..421a57b 100644 --- a/browser-extension/src/handlers/github-handler.ts +++ b/browser-extension/src/handlers/github-handler.ts @@ -1,4 +1,4 @@ -import { CommentContext, TextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; +import { CommentContext, TextareaHandler } from '../datamodel/textarea-handler'; export type GitHubCommentType = | 'GH_ISSUE_NEW' diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/handlers/reddit-handler.ts index 1a9e70a..c12f514 100644 --- a/browser-extension/src/handlers/reddit-handler.ts +++ b/browser-extension/src/handlers/reddit-handler.ts @@ -1,4 +1,4 @@ -import { CommentContext, TextareaHandler, TextareaInfo } from '../datamodel/textarea-handler'; +import { CommentContext, TextareaHandler } from '../datamodel/textarea-handler'; export type RedditCommentType = | 'REDDIT_POST_NEW' From aa073efe79b30a98ad980b94bc34037f8bf0ab50 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 17:14:32 -0700 Subject: [PATCH 13/54] Add a `TextAreaRegistry` for tracking the textareas themselves at runtime. --- browser-extension/src/datamodel/handler-registry.ts | 2 +- browser-extension/src/datamodel/textarea-handler.ts | 1 + .../src/datamodel/textarea-registry.ts | 13 +++++++++++++ browser-extension/src/entrypoints/content.ts | 10 ++++++---- 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 browser-extension/src/datamodel/textarea-registry.ts diff --git a/browser-extension/src/datamodel/handler-registry.ts b/browser-extension/src/datamodel/handler-registry.ts index 89074dd..154308b 100644 --- a/browser-extension/src/datamodel/handler-registry.ts +++ b/browser-extension/src/datamodel/handler-registry.ts @@ -29,7 +29,7 @@ export class HandlerRegistry { try { const context = handler.identifyContextOf(textarea); if (context) { - return { element: textarea, context }; + return { element: textarea, context, handler }; } } catch (error) { console.warn('Handler failed to identify textarea:', error); diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts index ccec6ca..a11bcbb 100644 --- a/browser-extension/src/datamodel/textarea-handler.ts +++ b/browser-extension/src/datamodel/textarea-handler.ts @@ -7,6 +7,7 @@ export interface CommentContext { export interface TextareaInfo { element: HTMLTextAreaElement; context: T; + handler: TextareaHandler; } export interface TextareaHandler { diff --git a/browser-extension/src/datamodel/textarea-registry.ts b/browser-extension/src/datamodel/textarea-registry.ts new file mode 100644 index 0000000..77bfcf3 --- /dev/null +++ b/browser-extension/src/datamodel/textarea-registry.ts @@ -0,0 +1,13 @@ +import { TextareaInfo, CommentContext } from './textarea-handler'; + +export class TextareaRegistry { + private textareas = new Map>(); + + register(textareaInfo: TextareaInfo): void { + this.textareas.set(textareaInfo.element, textareaInfo); + } + + get(textarea: HTMLTextAreaElement): TextareaInfo | undefined { + return this.textareas.get(textarea); + } +} \ No newline at end of file diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 27b804b..e91a16e 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -2,8 +2,10 @@ import { CONFIG } from './content/config' import { logger } from './content/logger' import { injectStyles } from './content/styles' import { HandlerRegistry } from '../datamodel/handler-registry' +import { TextareaRegistry } from '../datamodel/textarea-registry' -const registry = new HandlerRegistry() +const handlerRegistry = new HandlerRegistry() +const textareaRegistry = new TextareaRegistry() export default defineContentScript({ main() { @@ -16,7 +18,7 @@ export default defineContentScript({ childList: true, subtree: true, }) - logger.debug('Extension loaded with', registry.getAllHandlers().length, 'handlers') + logger.debug('Extension loaded with', handlerRegistry.getAllHandlers().length, 'handlers') }, matches: [''], runAt: 'document_end', @@ -49,10 +51,10 @@ function initializeMaybe(textarea: HTMLTextAreaElement) { textarea.classList.add(CONFIG.ADDED_OVERTYPE_CLASS) // Use registry to identify and handle this specific textarea - const textareaInfo = registry.identifyTextarea(textarea) + const textareaInfo = handlerRegistry.identifyTextarea(textarea) if (textareaInfo) { logger.debug('Identified textarea:', textareaInfo.context.type, textareaInfo.context.unique_key) - // TODO: Set up textarea monitoring and draft saving + textareaRegistry.register(textareaInfo) } else { logger.debug('No handler found for textarea') } From 3ab68d74670925246257003e9e1dc6ed960e13ad Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 17:17:49 -0700 Subject: [PATCH 14/54] No more marker classes, just our registry. --- browser-extension/src/entrypoints/content.ts | 38 ++++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index e91a16e..91bcc9e 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -1,4 +1,3 @@ -import { CONFIG } from './content/config' import { logger } from './content/logger' import { injectStyles } from './content/styles' import { HandlerRegistry } from '../datamodel/handler-registry' @@ -11,7 +10,7 @@ export default defineContentScript({ main() { const textAreasOnPageLoad = document.querySelectorAll(`textarea`) for (const textarea of textAreasOnPageLoad) { - initializeMaybe(textarea) + initializeMaybeIsPageload(textarea, true) } const observer = new MutationObserver(handleMutations) observer.observe(document.body, { @@ -30,13 +29,13 @@ function handleMutations(mutations: MutationRecord[]): void { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as Element if (element.tagName === 'TEXTAREA') { - initializeMaybe(element as HTMLTextAreaElement) + initializeMaybeIsPageload(element as HTMLTextAreaElement, false) } // Also check for textareas within added subtrees const textareas = element.querySelectorAll?.('textarea') if (textareas) { for (const textarea of textareas) { - initializeMaybe(textarea) + initializeMaybeIsPageload(textarea, false) } } } @@ -44,21 +43,22 @@ function handleMutations(mutations: MutationRecord[]): void { } } -function initializeMaybe(textarea: HTMLTextAreaElement) { - if (!textarea.classList.contains(CONFIG.ADDED_OVERTYPE_CLASS)) { - logger.debug('activating textarea {}', textarea) - injectStyles() - textarea.classList.add(CONFIG.ADDED_OVERTYPE_CLASS) - - // Use registry to identify and handle this specific textarea - const textareaInfo = handlerRegistry.identifyTextarea(textarea) - if (textareaInfo) { - logger.debug('Identified textarea:', textareaInfo.context.type, textareaInfo.context.unique_key) - textareaRegistry.register(textareaInfo) - } else { - logger.debug('No handler found for textarea') - } +function initializeMaybeIsPageload(textarea: HTMLTextAreaElement, isPageload: boolean) { + // Check if this textarea is already registered + if (textareaRegistry.get(textarea)) { + logger.debug('textarea already registered {}', textarea) + return + } + + logger.debug('activating textarea {}', textarea) + injectStyles() + + // Use registry to identify and handle this specific textarea + const textareaInfo = handlerRegistry.identifyTextarea(textarea) + if (textareaInfo) { + logger.debug('Identified textarea:', textareaInfo.context.type, textareaInfo.context.unique_key) + textareaRegistry.register(textareaInfo) } else { - logger.debug('already activated textarea {}', textarea) + logger.debug('No handler found for textarea') } } From 0f354d933548951b9a02a9776e17f274249d0a91 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 17:21:31 -0700 Subject: [PATCH 15/54] Unregistering a textarea works too. --- .../src/datamodel/textarea-registry.ts | 8 ++++++ browser-extension/src/entrypoints/content.ts | 26 ++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/browser-extension/src/datamodel/textarea-registry.ts b/browser-extension/src/datamodel/textarea-registry.ts index 77bfcf3..50e1efa 100644 --- a/browser-extension/src/datamodel/textarea-registry.ts +++ b/browser-extension/src/datamodel/textarea-registry.ts @@ -5,6 +5,14 @@ export class TextareaRegistry { register(textareaInfo: TextareaInfo): void { this.textareas.set(textareaInfo.element, textareaInfo); + // TODO: register as a draft in progress with the global list + } + + unregisterDueToModification(textarea: HTMLTextAreaElement): void { + if (this.textareas.has(textarea)) { + // TODO: register as abandoned or maybe submitted with the global list + this.textareas.delete(textarea); + } } get(textarea: HTMLTextAreaElement): TextareaInfo | undefined { diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 91bcc9e..b6cdea2 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -10,7 +10,7 @@ export default defineContentScript({ main() { const textAreasOnPageLoad = document.querySelectorAll(`textarea`) for (const textarea of textAreasOnPageLoad) { - initializeMaybeIsPageload(textarea, true) + initializeMaybeIsPageload(textarea) } const observer = new MutationObserver(handleMutations) observer.observe(document.body, { @@ -25,17 +25,35 @@ export default defineContentScript({ function handleMutations(mutations: MutationRecord[]): void { for (const mutation of mutations) { + // Handle added nodes for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as Element if (element.tagName === 'TEXTAREA') { - initializeMaybeIsPageload(element as HTMLTextAreaElement, false) + initializeMaybeIsPageload(element as HTMLTextAreaElement) } // Also check for textareas within added subtrees const textareas = element.querySelectorAll?.('textarea') if (textareas) { for (const textarea of textareas) { - initializeMaybeIsPageload(textarea, false) + initializeMaybeIsPageload(textarea) + } + } + } + } + + // Handle removed nodes + for (const node of mutation.removedNodes) { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element + if (element.tagName === 'TEXTAREA') { + textareaRegistry.unregisterDueToModification(element as HTMLTextAreaElement) + } + // Also check for textareas within removed subtrees + const textareas = element.querySelectorAll?.('textarea') + if (textareas) { + for (const textarea of textareas) { + textareaRegistry.unregisterDueToModification(textarea) } } } @@ -43,7 +61,7 @@ function handleMutations(mutations: MutationRecord[]): void { } } -function initializeMaybeIsPageload(textarea: HTMLTextAreaElement, isPageload: boolean) { +function initializeMaybeIsPageload(textarea: HTMLTextAreaElement) { // Check if this textarea is already registered if (textareaRegistry.get(textarea)) { logger.debug('textarea already registered {}', textarea) From 180b25864598114a507d30e2b137261655eb7bcb Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 19:59:54 -0700 Subject: [PATCH 16/54] very small fixup - don't do the query if the thing is itself a textarea --- browser-extension/src/entrypoints/content.ts | 32 +++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index b6cdea2..340d045 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -1,7 +1,7 @@ -import { logger } from './content/logger' -import { injectStyles } from './content/styles' import { HandlerRegistry } from '../datamodel/handler-registry' import { TextareaRegistry } from '../datamodel/textarea-registry' +import { logger } from './content/logger' +import { injectStyles } from './content/styles' const handlerRegistry = new HandlerRegistry() const textareaRegistry = new TextareaRegistry() @@ -31,12 +31,13 @@ function handleMutations(mutations: MutationRecord[]): void { const element = node as Element if (element.tagName === 'TEXTAREA') { initializeMaybeIsPageload(element as HTMLTextAreaElement) - } - // Also check for textareas within added subtrees - const textareas = element.querySelectorAll?.('textarea') - if (textareas) { - for (const textarea of textareas) { - initializeMaybeIsPageload(textarea) + } else { + // Also check for textareas within added subtrees + const textareas = element.querySelectorAll?.('textarea') + if (textareas) { + for (const textarea of textareas) { + initializeMaybeIsPageload(textarea) + } } } } @@ -48,12 +49,13 @@ function handleMutations(mutations: MutationRecord[]): void { const element = node as Element if (element.tagName === 'TEXTAREA') { textareaRegistry.unregisterDueToModification(element as HTMLTextAreaElement) - } - // Also check for textareas within removed subtrees - const textareas = element.querySelectorAll?.('textarea') - if (textareas) { - for (const textarea of textareas) { - textareaRegistry.unregisterDueToModification(textarea) + } else { + // Also check for textareas within removed subtrees + const textareas = element.querySelectorAll?.('textarea') + if (textareas) { + for (const textarea of textareas) { + textareaRegistry.unregisterDueToModification(textarea) + } } } } @@ -70,7 +72,7 @@ function initializeMaybeIsPageload(textarea: HTMLTextAreaElement) { logger.debug('activating textarea {}', textarea) injectStyles() - + // Use registry to identify and handle this specific textarea const textareaInfo = handlerRegistry.identifyTextarea(textarea) if (textareaInfo) { From 63f13ee8514cdbbed73e331b0bbb153488b7d65f Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 20:00:11 -0700 Subject: [PATCH 17/54] biome:fix --- .../src/datamodel/handler-registry.ts | 33 ++-- .../src/datamodel/textarea-handler.ts | 24 ++- .../src/datamodel/textarea-registry.ts | 12 +- .../src/handlers/github-handler.ts | 149 +++++++++--------- .../src/handlers/reddit-handler.ts | 142 ++++++++--------- 5 files changed, 173 insertions(+), 187 deletions(-) diff --git a/browser-extension/src/datamodel/handler-registry.ts b/browser-extension/src/datamodel/handler-registry.ts index 154308b..194bbe1 100644 --- a/browser-extension/src/datamodel/handler-registry.ts +++ b/browser-extension/src/datamodel/handler-registry.ts @@ -1,49 +1,48 @@ -import { CommentContext, TextareaHandler, TextareaInfo } from './textarea-handler'; -import { GitHubHandler } from '../handlers/github-handler'; -import { RedditHandler } from '../handlers/reddit-handler'; +import { GitHubHandler } from '../handlers/github-handler' +import { RedditHandler } from '../handlers/reddit-handler' +import type { CommentContext, TextareaHandler, TextareaInfo } from './textarea-handler' export class HandlerRegistry { - private handlers = new Set>(); + private handlers = new Set>() constructor() { // Register all available handlers - this.register(new GitHubHandler()); - this.register(new RedditHandler()); + this.register(new GitHubHandler()) + this.register(new RedditHandler()) } private register(handler: TextareaHandler): void { - this.handlers.add(handler); + this.handlers.add(handler) } getHandlerForType(type: string): TextareaHandler | null { for (const handler of this.handlers) { if (handler.forCommentTypes().includes(type)) { - return handler; + return handler } } - return null; + return null } identifyTextarea(textarea: HTMLTextAreaElement): TextareaInfo | null { for (const handler of this.handlers) { try { - const context = handler.identifyContextOf(textarea); + const context = handler.identifyContextOf(textarea) if (context) { - return { element: textarea, context, handler }; + return { context, element: textarea, handler } } } catch (error) { - console.warn('Handler failed to identify textarea:', error); + console.warn('Handler failed to identify textarea:', error) } } - return null; + return null } - getAllHandlers(): TextareaHandler[] { - return Array.from(this.handlers); + return Array.from(this.handlers) } getCommentTypesForHandler(handler: TextareaHandler): string[] { - return handler.forCommentTypes(); + return handler.forCommentTypes() } -} \ No newline at end of file +} diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts index a11bcbb..74939fb 100644 --- a/browser-extension/src/datamodel/textarea-handler.ts +++ b/browser-extension/src/datamodel/textarea-handler.ts @@ -1,24 +1,22 @@ - export interface CommentContext { - unique_key: string; - type: string; + unique_key: string + type: string } export interface TextareaInfo { - element: HTMLTextAreaElement; - context: T; - handler: TextareaHandler; + element: HTMLTextAreaElement + context: T + handler: TextareaHandler } export interface TextareaHandler { // Handler metadata - forCommentTypes(): string[]; + forCommentTypes(): string[] // whenever a new `textarea` is added to any webpage, this method is called to try to find a handler for it - identifyContextOf(textarea: HTMLTextAreaElement): T | null; - + identifyContextOf(textarea: HTMLTextAreaElement): T | null + // Popup functionality helpers - generateDisplayTitle(context: T): string; - generateIcon(context: T): string; - buildUrl(context: T, withDraft?: boolean): string; + generateDisplayTitle(context: T): string + generateIcon(context: T): string + buildUrl(context: T, withDraft?: boolean): string } - diff --git a/browser-extension/src/datamodel/textarea-registry.ts b/browser-extension/src/datamodel/textarea-registry.ts index 50e1efa..a38cd4a 100644 --- a/browser-extension/src/datamodel/textarea-registry.ts +++ b/browser-extension/src/datamodel/textarea-registry.ts @@ -1,21 +1,21 @@ -import { TextareaInfo, CommentContext } from './textarea-handler'; +import type { CommentContext, TextareaInfo } from './textarea-handler' export class TextareaRegistry { - private textareas = new Map>(); + private textareas = new Map>() register(textareaInfo: TextareaInfo): void { - this.textareas.set(textareaInfo.element, textareaInfo); + this.textareas.set(textareaInfo.element, textareaInfo) // TODO: register as a draft in progress with the global list } unregisterDueToModification(textarea: HTMLTextAreaElement): void { if (this.textareas.has(textarea)) { // TODO: register as abandoned or maybe submitted with the global list - this.textareas.delete(textarea); + this.textareas.delete(textarea) } } get(textarea: HTMLTextAreaElement): TextareaInfo | undefined { - return this.textareas.get(textarea); + return this.textareas.get(textarea) } -} \ No newline at end of file +} diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/handlers/github-handler.ts index 421a57b..b47b584 100644 --- a/browser-extension/src/handlers/github-handler.ts +++ b/browser-extension/src/handlers/github-handler.ts @@ -1,24 +1,23 @@ -import { CommentContext, TextareaHandler } from '../datamodel/textarea-handler'; +import type { CommentContext, TextareaHandler } from '../datamodel/textarea-handler' -export type GitHubCommentType = +export type GitHubCommentType = | 'GH_ISSUE_NEW' | 'GH_PR_NEW' | 'GH_ISSUE_ADD_COMMENT' | 'GH_ISSUE_EDIT_COMMENT' | 'GH_PR_ADD_COMMENT' | 'GH_PR_EDIT_COMMENT' - | 'GH_PR_CODE_COMMENT'; + | 'GH_PR_CODE_COMMENT' export interface GitHubContext extends CommentContext { - type: GitHubCommentType; // Override to narrow from string to specific union - domain: string; - slug: string; // owner/repo - number?: number | undefined; // issue/PR number - commentId?: string | undefined; // for editing existing comments + type: GitHubCommentType // Override to narrow from string to specific union + domain: string + slug: string // owner/repo + number?: number | undefined // issue/PR number + commentId?: string | undefined // for editing existing comments } export class GitHubHandler implements TextareaHandler { - forCommentTypes(): string[] { return [ 'GH_ISSUE_NEW', @@ -27,97 +26,98 @@ export class GitHubHandler implements TextareaHandler { 'GH_ISSUE_EDIT_COMMENT', 'GH_PR_ADD_COMMENT', 'GH_PR_EDIT_COMMENT', - 'GH_PR_CODE_COMMENT' - ]; + 'GH_PR_CODE_COMMENT', + ] } identifyContextOf(textarea: HTMLTextAreaElement): GitHubContext | null { // Only handle GitHub domains if (!window.location.hostname.includes('github')) { - return null; + return null } - const pathname = window.location.pathname; - + const pathname = window.location.pathname + // Parse GitHub URL structure: /owner/repo/issues/123 or /owner/repo/pull/456 - const match = pathname.match(/^\/([^\/]+)\/([^\/]+)(?:\/(issues|pull)\/(\d+))?/); - if (!match) return null; + const match = pathname.match(/^\/([^/]+)\/([^/]+)(?:\/(issues|pull)\/(\d+))?/) + if (!match) return null + + const [, owner, repo, urlType, numberStr] = match + const slug = `${owner}/${repo}` + const number = numberStr ? parseInt(numberStr, 10) : undefined - const [, owner, repo, urlType, numberStr] = match; - const slug = `${owner}/${repo}`; - const number = numberStr ? parseInt(numberStr, 10) : undefined; - // Check if editing existing comment - const commentId = this.getCommentId(textarea); - + const commentId = this.getCommentId(textarea) + // Determine comment type - let type: GitHubCommentType; - + let type: GitHubCommentType + // New issue if (pathname.includes('/issues/new')) { - type = 'GH_ISSUE_NEW'; + type = 'GH_ISSUE_NEW' } // New PR else if (pathname.includes('/compare/') || pathname.endsWith('/compare')) { - type = 'GH_PR_NEW'; + type = 'GH_PR_NEW' } // Existing issue or PR page else if (urlType && number) { - const isEditingComment = commentId !== null; - + const isEditingComment = commentId !== null + if (urlType === 'issues') { - type = isEditingComment ? 'GH_ISSUE_EDIT_COMMENT' : 'GH_ISSUE_ADD_COMMENT'; + type = isEditingComment ? 'GH_ISSUE_EDIT_COMMENT' : 'GH_ISSUE_ADD_COMMENT' } else { // Check if it's a code comment (in Files Changed tab) - const isCodeComment = textarea.closest('.js-inline-comment-form') !== null || - textarea.closest('[data-path]') !== null; - + const isCodeComment = + textarea.closest('.js-inline-comment-form') !== null || + textarea.closest('[data-path]') !== null + if (isCodeComment) { - type = 'GH_PR_CODE_COMMENT'; + type = 'GH_PR_CODE_COMMENT' } else { - type = isEditingComment ? 'GH_PR_EDIT_COMMENT' : 'GH_PR_ADD_COMMENT'; + type = isEditingComment ? 'GH_PR_EDIT_COMMENT' : 'GH_PR_ADD_COMMENT' } } } else { - return null; + return null } - + // Generate unique key based on context - let unique_key = `github:${slug}`; + let unique_key = `github:${slug}` if (number) { - unique_key += `:${urlType}:${number}`; + unique_key += `:${urlType}:${number}` } else { - unique_key += ':new'; + unique_key += ':new' } - + if (commentId) { - unique_key += `:edit:${commentId}`; + unique_key += `:edit:${commentId}` } const context: GitHubContext = { - unique_key, - type, + commentId: commentId || undefined, domain: window.location.hostname, - slug, number, - commentId: commentId || undefined - }; + slug, + type, + unique_key, + } - return context; + return context } generateDisplayTitle(context: GitHubContext): string { - const { slug, number, commentId } = context; - + const { slug, number, commentId } = context + if (commentId) { - return `Edit comment in ${slug}${number ? ` #${number}` : ''}`; + return `Edit comment in ${slug}${number ? ` #${number}` : ''}` } - + if (number) { - return `Comment on ${slug} #${number}`; + return `Comment on ${slug} #${number}` } - - return `New ${window.location.pathname.includes('/issues/') ? 'issue' : 'PR'} in ${slug}`; + + return `New ${window.location.pathname.includes('/issues/') ? 'issue' : 'PR'} in ${slug}` } generateIcon(context: GitHubContext): string { @@ -125,46 +125,45 @@ export class GitHubHandler implements TextareaHandler { case 'GH_ISSUE_NEW': case 'GH_ISSUE_ADD_COMMENT': case 'GH_ISSUE_EDIT_COMMENT': - return '🐛'; // Issue icon + return '🐛' // Issue icon case 'GH_PR_NEW': case 'GH_PR_ADD_COMMENT': case 'GH_PR_EDIT_COMMENT': - return '🔄'; // PR icon + return '🔄' // PR icon case 'GH_PR_CODE_COMMENT': - return '💬'; // Code comment icon + return '💬' // Code comment icon default: - return '📝'; // Generic comment icon + return '📝' // Generic comment icon } } buildUrl(context: GitHubContext): string { - const baseUrl = `https://${context.domain}/${context.slug}`; - + const baseUrl = `https://${context.domain}/${context.slug}` + if (context.number) { - const type = window.location.pathname.includes('/issues/') ? 'issues' : 'pull'; - return `${baseUrl}/${type}/${context.number}${context.commentId ? `#issuecomment-${context.commentId}` : ''}`; + const type = window.location.pathname.includes('/issues/') ? 'issues' : 'pull' + return `${baseUrl}/${type}/${context.number}${context.commentId ? `#issuecomment-${context.commentId}` : ''}` } - - return baseUrl; + + return baseUrl } private getCommentId(textarea: HTMLTextAreaElement): string | null { // Look for edit comment form indicators - const commentForm = textarea.closest('[data-comment-id]'); + const commentForm = textarea.closest('[data-comment-id]') if (commentForm) { - return commentForm.getAttribute('data-comment-id'); + return commentForm.getAttribute('data-comment-id') } - - const editForm = textarea.closest('.js-comment-edit-form'); + + const editForm = textarea.closest('.js-comment-edit-form') if (editForm) { - const commentContainer = editForm.closest('.js-comment-container'); + const commentContainer = editForm.closest('.js-comment-container') if (commentContainer) { - const id = commentContainer.getAttribute('data-gid') || - commentContainer.getAttribute('id'); - return id ? id.replace('issuecomment-', '') : null; + const id = commentContainer.getAttribute('data-gid') || commentContainer.getAttribute('id') + return id ? id.replace('issuecomment-', '') : null } } - - return null; + + return null } -} \ No newline at end of file +} diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/handlers/reddit-handler.ts index c12f514..9cfa943 100644 --- a/browser-extension/src/handlers/reddit-handler.ts +++ b/browser-extension/src/handlers/reddit-handler.ts @@ -1,149 +1,139 @@ -import { CommentContext, TextareaHandler } from '../datamodel/textarea-handler'; +import type { CommentContext, TextareaHandler } from '../datamodel/textarea-handler' -export type RedditCommentType = - | 'REDDIT_POST_NEW' - | 'REDDIT_COMMENT_NEW' - | 'REDDIT_COMMENT_EDIT'; +export type RedditCommentType = 'REDDIT_POST_NEW' | 'REDDIT_COMMENT_NEW' | 'REDDIT_COMMENT_EDIT' export interface RedditContext extends CommentContext { - type: RedditCommentType; // Override to narrow from string to specific union - subreddit: string; - postId?: string | undefined; - commentId?: string | undefined; // for editing existing comments + type: RedditCommentType // Override to narrow from string to specific union + subreddit: string + postId?: string | undefined + commentId?: string | undefined // for editing existing comments } export class RedditHandler implements TextareaHandler { - forCommentTypes(): string[] { - return [ - 'REDDIT_POST_NEW', - 'REDDIT_COMMENT_NEW', - 'REDDIT_COMMENT_EDIT' - ]; + return ['REDDIT_POST_NEW', 'REDDIT_COMMENT_NEW', 'REDDIT_COMMENT_EDIT'] } identifyContextOf(textarea: HTMLTextAreaElement): RedditContext | null { // Only handle Reddit domains if (!window.location.hostname.includes('reddit')) { - return null; + return null } - const pathname = window.location.pathname; - + const pathname = window.location.pathname + // Parse Reddit URL structure: /r/subreddit/comments/postid/title/ - const postMatch = pathname.match(/^\/r\/([^\/]+)\/comments\/([^\/]+)/); - const submitMatch = pathname.match(/^\/r\/([^\/]+)\/submit/); - const subredditMatch = pathname.match(/^\/r\/([^\/]+)/); - - let subreddit: string | undefined; - let postId: string | undefined; - + const postMatch = pathname.match(/^\/r\/([^/]+)\/comments\/([^/]+)/) + const submitMatch = pathname.match(/^\/r\/([^/]+)\/submit/) + const subredditMatch = pathname.match(/^\/r\/([^/]+)/) + + let subreddit: string | undefined + let postId: string | undefined + if (postMatch) { - [, subreddit, postId] = postMatch; + ;[, subreddit, postId] = postMatch } else if (submitMatch) { - [, subreddit] = submitMatch; + ;[, subreddit] = submitMatch } else if (subredditMatch) { - [, subreddit] = subredditMatch; + ;[, subreddit] = subredditMatch } - + if (!subreddit) { - return null; + return null } // Check if editing existing comment - const commentId = this.getCommentId(textarea); - + const commentId = this.getCommentId(textarea) + // Determine comment type - let type: RedditCommentType; - + let type: RedditCommentType + // New post submission if (pathname.includes('/submit')) { - type = 'REDDIT_POST_NEW'; + type = 'REDDIT_POST_NEW' } // Check if we're on a post page - else if (pathname.match(/\/r\/[^\/]+\/comments\/[^\/]+/)) { - const isEditingComment = commentId !== null; - type = isEditingComment ? 'REDDIT_COMMENT_EDIT' : 'REDDIT_COMMENT_NEW'; + else if (pathname.match(/\/r\/[^/]+\/comments\/[^/]+/)) { + const isEditingComment = commentId !== null + type = isEditingComment ? 'REDDIT_COMMENT_EDIT' : 'REDDIT_COMMENT_NEW' } else { - return null; + return null } // Generate unique key - let unique_key = `reddit:${subreddit}`; + let unique_key = `reddit:${subreddit}` if (postId) { - unique_key += `:${postId}`; + unique_key += `:${postId}` } else { - unique_key += ':new'; + unique_key += ':new' } if (commentId) { - unique_key += `:edit:${commentId}`; + unique_key += `:edit:${commentId}` } const context: RedditContext = { - unique_key, - type, - subreddit, + commentId: commentId || undefined, postId, - commentId: commentId || undefined - }; + subreddit, + type, + unique_key, + } - return context; + return context } generateDisplayTitle(context: RedditContext): string { - const { subreddit, postId, commentId } = context; - + const { subreddit, postId, commentId } = context + if (commentId) { - return `Edit comment in r/${subreddit}`; + return `Edit comment in r/${subreddit}` } - + if (postId) { - return `Comment in r/${subreddit}`; + return `Comment in r/${subreddit}` } - - return `New post in r/${subreddit}`; + + return `New post in r/${subreddit}` } generateIcon(context: RedditContext): string { switch (context.type) { case 'REDDIT_POST_NEW': - return '📝'; // Post icon + return '📝' // Post icon case 'REDDIT_COMMENT_NEW': - return '💬'; // Comment icon + return '💬' // Comment icon case 'REDDIT_COMMENT_EDIT': - return '✏️'; // Edit icon + return '✏️' // Edit icon default: - return '🔵'; // Reddit icon + return '🔵' // Reddit icon } } buildUrl(context: RedditContext): string { - const baseUrl = `https://reddit.com/r/${context.subreddit}`; - + const baseUrl = `https://reddit.com/r/${context.subreddit}` + if (context.postId) { - return `${baseUrl}/comments/${context.postId}/${context.commentId ? `#${context.commentId}` : ''}`; + return `${baseUrl}/comments/${context.postId}/${context.commentId ? `#${context.commentId}` : ''}` } - - return baseUrl; + + return baseUrl } private getCommentId(textarea: HTMLTextAreaElement): string | null { // Look for edit comment form indicators - const commentForm = textarea.closest('[data-comment-id]'); + const commentForm = textarea.closest('[data-comment-id]') if (commentForm) { - return commentForm.getAttribute('data-comment-id'); + return commentForm.getAttribute('data-comment-id') } - + // Reddit uses different class names, check for common edit form patterns - const editForm = textarea.closest('.edit-usertext') || - textarea.closest('[data-type="comment"]'); + const editForm = textarea.closest('.edit-usertext') || textarea.closest('[data-type="comment"]') if (editForm) { - const id = editForm.getAttribute('data-fullname') || - editForm.getAttribute('data-comment-id'); - return id; + const id = editForm.getAttribute('data-fullname') || editForm.getAttribute('data-comment-id') + return id } - - return null; + + return null } -} \ No newline at end of file +} From 7d845f5003e669e25ff20fe2a9476a165ac82f0c Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 21:00:23 -0700 Subject: [PATCH 18/54] allow explicit any --- browser-extension/biome.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/browser-extension/biome.json b/browser-extension/biome.json index 9f5a110..d4cccec 100644 --- a/browser-extension/biome.json +++ b/browser-extension/biome.json @@ -41,7 +41,7 @@ "linter": { "rules": { "complexity": { - "noExcessiveCognitiveComplexity": "warn" + "noExcessiveCognitiveComplexity": "off" }, "correctness": { "noUnusedVariables": "error", @@ -65,7 +65,7 @@ "allow": ["assert", "error", "info", "warn"] } }, - "noExplicitAny": "error", + "noExplicitAny": "off", "noVar": "error" } } From e48454138d2c82097c9e0358a53cdd3bf6bd46a5 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 21:00:30 -0700 Subject: [PATCH 19/54] Rename --- browser-extension/src/datamodel/enhancer.ts | 24 ++++++ .../src/datamodel/handler-registry.ts | 48 ------------ .../handlers/github-handler.ts | 4 +- .../handlers/reddit-handler.ts | 4 +- browser-extension/src/datamodel/registries.ts | 74 +++++++++++++++++++ .../src/datamodel/textarea-handler.ts | 22 ------ .../src/datamodel/textarea-registry.ts | 21 ------ browser-extension/src/entrypoints/content.ts | 5 +- 8 files changed, 104 insertions(+), 98 deletions(-) create mode 100644 browser-extension/src/datamodel/enhancer.ts delete mode 100644 browser-extension/src/datamodel/handler-registry.ts rename browser-extension/src/{ => datamodel}/handlers/github-handler.ts (96%) rename browser-extension/src/{ => datamodel}/handlers/reddit-handler.ts (96%) create mode 100644 browser-extension/src/datamodel/registries.ts delete mode 100644 browser-extension/src/datamodel/textarea-handler.ts delete mode 100644 browser-extension/src/datamodel/textarea-registry.ts diff --git a/browser-extension/src/datamodel/enhancer.ts b/browser-extension/src/datamodel/enhancer.ts new file mode 100644 index 0000000..7b51863 --- /dev/null +++ b/browser-extension/src/datamodel/enhancer.ts @@ -0,0 +1,24 @@ +/** + * stores enough info about the location of a draft to: + * - display it in a table + * - reopen the draft in-context + */ +export interface CommentContext { + unique_key: string + type: string +} + +/** wraps the textareas of a given platform with Gitcasso's enhancements */ +export interface CommentEnhancer { + /** guarantees to only return a type within this list */ + forCommentTypes(): string[] + /** + * whenever a new `textarea` is added to any webpage, this method is called. + * if we return non-null, then we become the handler for that text area. + */ + identifyContextOf(textarea: HTMLTextAreaElement): T | null + + generateIcon(context: T): string + generateDisplayTitle(context: T): string + buildUrl(context: T, withDraft?: boolean): string +} diff --git a/browser-extension/src/datamodel/handler-registry.ts b/browser-extension/src/datamodel/handler-registry.ts deleted file mode 100644 index 194bbe1..0000000 --- a/browser-extension/src/datamodel/handler-registry.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { GitHubHandler } from '../handlers/github-handler' -import { RedditHandler } from '../handlers/reddit-handler' -import type { CommentContext, TextareaHandler, TextareaInfo } from './textarea-handler' - -export class HandlerRegistry { - private handlers = new Set>() - - constructor() { - // Register all available handlers - this.register(new GitHubHandler()) - this.register(new RedditHandler()) - } - - private register(handler: TextareaHandler): void { - this.handlers.add(handler) - } - - getHandlerForType(type: string): TextareaHandler | null { - for (const handler of this.handlers) { - if (handler.forCommentTypes().includes(type)) { - return handler - } - } - return null - } - - identifyTextarea(textarea: HTMLTextAreaElement): TextareaInfo | null { - for (const handler of this.handlers) { - try { - const context = handler.identifyContextOf(textarea) - if (context) { - return { context, element: textarea, handler } - } - } catch (error) { - console.warn('Handler failed to identify textarea:', error) - } - } - return null - } - - getAllHandlers(): TextareaHandler[] { - return Array.from(this.handlers) - } - - getCommentTypesForHandler(handler: TextareaHandler): string[] { - return handler.forCommentTypes() - } -} diff --git a/browser-extension/src/handlers/github-handler.ts b/browser-extension/src/datamodel/handlers/github-handler.ts similarity index 96% rename from browser-extension/src/handlers/github-handler.ts rename to browser-extension/src/datamodel/handlers/github-handler.ts index b47b584..2268873 100644 --- a/browser-extension/src/handlers/github-handler.ts +++ b/browser-extension/src/datamodel/handlers/github-handler.ts @@ -1,4 +1,4 @@ -import type { CommentContext, TextareaHandler } from '../datamodel/textarea-handler' +import type { CommentContext, CommentEnhancer } from '../enhancer' export type GitHubCommentType = | 'GH_ISSUE_NEW' @@ -17,7 +17,7 @@ export interface GitHubContext extends CommentContext { commentId?: string | undefined // for editing existing comments } -export class GitHubHandler implements TextareaHandler { +export class GitHubHandler implements CommentEnhancer { forCommentTypes(): string[] { return [ 'GH_ISSUE_NEW', diff --git a/browser-extension/src/handlers/reddit-handler.ts b/browser-extension/src/datamodel/handlers/reddit-handler.ts similarity index 96% rename from browser-extension/src/handlers/reddit-handler.ts rename to browser-extension/src/datamodel/handlers/reddit-handler.ts index 9cfa943..cec9587 100644 --- a/browser-extension/src/handlers/reddit-handler.ts +++ b/browser-extension/src/datamodel/handlers/reddit-handler.ts @@ -1,4 +1,4 @@ -import type { CommentContext, TextareaHandler } from '../datamodel/textarea-handler' +import type { CommentContext, CommentEnhancer } from '../enhancer' export type RedditCommentType = 'REDDIT_POST_NEW' | 'REDDIT_COMMENT_NEW' | 'REDDIT_COMMENT_EDIT' @@ -9,7 +9,7 @@ export interface RedditContext extends CommentContext { commentId?: string | undefined // for editing existing comments } -export class RedditHandler implements TextareaHandler { +export class RedditHandler implements CommentEnhancer { forCommentTypes(): string[] { return ['REDDIT_POST_NEW', 'REDDIT_COMMENT_NEW', 'REDDIT_COMMENT_EDIT'] } diff --git a/browser-extension/src/datamodel/registries.ts b/browser-extension/src/datamodel/registries.ts new file mode 100644 index 0000000..39e814f --- /dev/null +++ b/browser-extension/src/datamodel/registries.ts @@ -0,0 +1,74 @@ +import type { CommentContext, CommentEnhancer } from './enhancer' +import { GitHubHandler as GitHubEnhancer } from './handlers/github-handler' +import { RedditHandler as RedditEnhancer } from './handlers/reddit-handler' + +export interface EnhancedTextarea { + element: HTMLTextAreaElement + context: T + handler: CommentEnhancer +} + +export class EnhancerRegistry { + private enhancers = new Set>() + + constructor() { + // Register all available handlers + this.register(new GitHubEnhancer()) + this.register(new RedditEnhancer()) + } + + private register(handler: CommentEnhancer): void { + this.enhancers.add(handler) + } + + getHandlerForType(type: string): CommentEnhancer | null { + for (const handler of this.enhancers) { + if (handler.forCommentTypes().includes(type)) { + return handler + } + } + return null + } + + identifyTextarea(textarea: HTMLTextAreaElement): EnhancedTextarea | null { + for (const handler of this.enhancers) { + try { + const context = handler.identifyContextOf(textarea) + if (context) { + return { context, element: textarea, handler } + } + } catch (error) { + console.warn('Handler failed to identify textarea:', error) + } + } + return null + } + + getAllHandlers(): CommentEnhancer[] { + return Array.from(this.enhancers) + } + + getCommentTypesForHandler(handler: CommentEnhancer): string[] { + return handler.forCommentTypes() + } +} + +export class TextareaRegistry { + private textareas = new Map>() + + register(textareaInfo: EnhancedTextarea): void { + this.textareas.set(textareaInfo.element, textareaInfo) + // TODO: register as a draft in progress with the global list + } + + unregisterDueToModification(textarea: HTMLTextAreaElement): void { + if (this.textareas.has(textarea)) { + // TODO: register as abandoned or maybe submitted with the global list + this.textareas.delete(textarea) + } + } + + get(textarea: HTMLTextAreaElement): EnhancedTextarea | undefined { + return this.textareas.get(textarea) + } +} diff --git a/browser-extension/src/datamodel/textarea-handler.ts b/browser-extension/src/datamodel/textarea-handler.ts deleted file mode 100644 index 74939fb..0000000 --- a/browser-extension/src/datamodel/textarea-handler.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface CommentContext { - unique_key: string - type: string -} - -export interface TextareaInfo { - element: HTMLTextAreaElement - context: T - handler: TextareaHandler -} - -export interface TextareaHandler { - // Handler metadata - forCommentTypes(): string[] - // whenever a new `textarea` is added to any webpage, this method is called to try to find a handler for it - identifyContextOf(textarea: HTMLTextAreaElement): T | null - - // Popup functionality helpers - generateDisplayTitle(context: T): string - generateIcon(context: T): string - buildUrl(context: T, withDraft?: boolean): string -} diff --git a/browser-extension/src/datamodel/textarea-registry.ts b/browser-extension/src/datamodel/textarea-registry.ts deleted file mode 100644 index a38cd4a..0000000 --- a/browser-extension/src/datamodel/textarea-registry.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { CommentContext, TextareaInfo } from './textarea-handler' - -export class TextareaRegistry { - private textareas = new Map>() - - register(textareaInfo: TextareaInfo): void { - this.textareas.set(textareaInfo.element, textareaInfo) - // TODO: register as a draft in progress with the global list - } - - unregisterDueToModification(textarea: HTMLTextAreaElement): void { - if (this.textareas.has(textarea)) { - // TODO: register as abandoned or maybe submitted with the global list - this.textareas.delete(textarea) - } - } - - get(textarea: HTMLTextAreaElement): TextareaInfo | undefined { - return this.textareas.get(textarea) - } -} diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 340d045..92e846f 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -1,9 +1,8 @@ -import { HandlerRegistry } from '../datamodel/handler-registry' -import { TextareaRegistry } from '../datamodel/textarea-registry' +import { EnhancerRegistry, TextareaRegistry } from '../datamodel/registries' import { logger } from './content/logger' import { injectStyles } from './content/styles' -const handlerRegistry = new HandlerRegistry() +const handlerRegistry = new EnhancerRegistry() const textareaRegistry = new TextareaRegistry() export default defineContentScript({ From 33a2aba47a4deed7c73d1610cc789bb99e7ae72c Mon Sep 17 00:00:00 2001 From: ntwigg Date: Wed, 3 Sep 2025 21:04:06 -0700 Subject: [PATCH 20/54] More renaming and moving. --- .../{entrypoints/content => common}/config.ts | 0 .../{entrypoints/content => common}/logger.ts | 0 browser-extension/src/entrypoints/content.ts | 19 +++++++++++++++++-- .../src/entrypoints/content/styles.ts | 16 ---------------- 4 files changed, 17 insertions(+), 18 deletions(-) rename browser-extension/src/{entrypoints/content => common}/config.ts (100%) rename browser-extension/src/{entrypoints/content => common}/logger.ts (100%) delete mode 100644 browser-extension/src/entrypoints/content/styles.ts diff --git a/browser-extension/src/entrypoints/content/config.ts b/browser-extension/src/common/config.ts similarity index 100% rename from browser-extension/src/entrypoints/content/config.ts rename to browser-extension/src/common/config.ts diff --git a/browser-extension/src/entrypoints/content/logger.ts b/browser-extension/src/common/logger.ts similarity index 100% rename from browser-extension/src/entrypoints/content/logger.ts rename to browser-extension/src/common/logger.ts diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 92e846f..8a767d0 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -1,6 +1,6 @@ +import { CONFIG } from '../common/config' +import { logger } from '../common/logger' import { EnhancerRegistry, TextareaRegistry } from '../datamodel/registries' -import { logger } from './content/logger' -import { injectStyles } from './content/styles' const handlerRegistry = new EnhancerRegistry() const textareaRegistry = new TextareaRegistry() @@ -81,3 +81,18 @@ function initializeMaybeIsPageload(textarea: HTMLTextAreaElement) { logger.debug('No handler found for textarea') } } + +const STYLES = ` +.${CONFIG.ADDED_OVERTYPE_CLASS} { + background: cyan !important; +} +` + +function injectStyles(): void { + if (!document.getElementById('gitcasso-styles')) { + const style = document.createElement('style') + style.textContent = STYLES + style.id = 'gitcasso-styles' + document.head.appendChild(style) + } +} diff --git a/browser-extension/src/entrypoints/content/styles.ts b/browser-extension/src/entrypoints/content/styles.ts deleted file mode 100644 index 6d27449..0000000 --- a/browser-extension/src/entrypoints/content/styles.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { CONFIG } from './config' - -const STYLES = ` -.${CONFIG.ADDED_OVERTYPE_CLASS} { - background: cyan !important; -} -` - -export function injectStyles(): void { - if (!document.getElementById('gitcasso-styles')) { - const style = document.createElement('style') - style.textContent = STYLES - style.id = 'gitcasso-styles' - document.head.appendChild(style) - } -} From b3f7730f68978b4865e71c4baedee1d198c39c08 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 08:29:04 -0700 Subject: [PATCH 21/54] Rename refactor. --- browser-extension/src/datamodel/enhancer.ts | 4 +-- .../src/datamodel/handlers/github-handler.ts | 4 +-- .../src/datamodel/handlers/reddit-handler.ts | 4 +-- browser-extension/src/datamodel/registries.ts | 8 ++--- browser-extension/src/entrypoints/content.ts | 32 +++++++++++-------- 5 files changed, 28 insertions(+), 24 deletions(-) diff --git a/browser-extension/src/datamodel/enhancer.ts b/browser-extension/src/datamodel/enhancer.ts index 7b51863..3809bf4 100644 --- a/browser-extension/src/datamodel/enhancer.ts +++ b/browser-extension/src/datamodel/enhancer.ts @@ -3,13 +3,13 @@ * - display it in a table * - reopen the draft in-context */ -export interface CommentContext { +export interface CommentSpot { unique_key: string type: string } /** wraps the textareas of a given platform with Gitcasso's enhancements */ -export interface CommentEnhancer { +export interface CommentEnhancer { /** guarantees to only return a type within this list */ forCommentTypes(): string[] /** diff --git a/browser-extension/src/datamodel/handlers/github-handler.ts b/browser-extension/src/datamodel/handlers/github-handler.ts index 2268873..4ff6795 100644 --- a/browser-extension/src/datamodel/handlers/github-handler.ts +++ b/browser-extension/src/datamodel/handlers/github-handler.ts @@ -1,4 +1,4 @@ -import type { CommentContext, CommentEnhancer } from '../enhancer' +import type { CommentEnhancer, CommentSpot } from '../enhancer' export type GitHubCommentType = | 'GH_ISSUE_NEW' @@ -9,7 +9,7 @@ export type GitHubCommentType = | 'GH_PR_EDIT_COMMENT' | 'GH_PR_CODE_COMMENT' -export interface GitHubContext extends CommentContext { +export interface GitHubContext extends CommentSpot { type: GitHubCommentType // Override to narrow from string to specific union domain: string slug: string // owner/repo diff --git a/browser-extension/src/datamodel/handlers/reddit-handler.ts b/browser-extension/src/datamodel/handlers/reddit-handler.ts index cec9587..d7f677b 100644 --- a/browser-extension/src/datamodel/handlers/reddit-handler.ts +++ b/browser-extension/src/datamodel/handlers/reddit-handler.ts @@ -1,8 +1,8 @@ -import type { CommentContext, CommentEnhancer } from '../enhancer' +import type { CommentEnhancer, CommentSpot } from '../enhancer' export type RedditCommentType = 'REDDIT_POST_NEW' | 'REDDIT_COMMENT_NEW' | 'REDDIT_COMMENT_EDIT' -export interface RedditContext extends CommentContext { +export interface RedditContext extends CommentSpot { type: RedditCommentType // Override to narrow from string to specific union subreddit: string postId?: string | undefined diff --git a/browser-extension/src/datamodel/registries.ts b/browser-extension/src/datamodel/registries.ts index 39e814f..7200995 100644 --- a/browser-extension/src/datamodel/registries.ts +++ b/browser-extension/src/datamodel/registries.ts @@ -1,8 +1,8 @@ -import type { CommentContext, CommentEnhancer } from './enhancer' +import type { CommentEnhancer, CommentSpot } from './enhancer' import { GitHubHandler as GitHubEnhancer } from './handlers/github-handler' import { RedditHandler as RedditEnhancer } from './handlers/reddit-handler' -export interface EnhancedTextarea { +export interface EnhancedTextarea { element: HTMLTextAreaElement context: T handler: CommentEnhancer @@ -17,7 +17,7 @@ export class EnhancerRegistry { this.register(new RedditEnhancer()) } - private register(handler: CommentEnhancer): void { + private register(handler: CommentEnhancer): void { this.enhancers.add(handler) } @@ -56,7 +56,7 @@ export class EnhancerRegistry { export class TextareaRegistry { private textareas = new Map>() - register(textareaInfo: EnhancedTextarea): void { + register(textareaInfo: EnhancedTextarea): void { this.textareas.set(textareaInfo.element, textareaInfo) // TODO: register as a draft in progress with the global list } diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 8a767d0..2716229 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -2,21 +2,21 @@ import { CONFIG } from '../common/config' import { logger } from '../common/logger' import { EnhancerRegistry, TextareaRegistry } from '../datamodel/registries' -const handlerRegistry = new EnhancerRegistry() -const textareaRegistry = new TextareaRegistry() +const enhancers = new EnhancerRegistry() +const enhancedTextareas = new TextareaRegistry() export default defineContentScript({ main() { const textAreasOnPageLoad = document.querySelectorAll(`textarea`) for (const textarea of textAreasOnPageLoad) { - initializeMaybeIsPageload(textarea) + enhanceMaybe(textarea) } const observer = new MutationObserver(handleMutations) observer.observe(document.body, { childList: true, subtree: true, }) - logger.debug('Extension loaded with', handlerRegistry.getAllHandlers().length, 'handlers') + logger.debug('Extension loaded with', enhancers.getAllHandlers().length, 'handlers') }, matches: [''], runAt: 'document_end', @@ -29,13 +29,13 @@ function handleMutations(mutations: MutationRecord[]): void { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as Element if (element.tagName === 'TEXTAREA') { - initializeMaybeIsPageload(element as HTMLTextAreaElement) + enhanceMaybe(element as HTMLTextAreaElement) } else { // Also check for textareas within added subtrees const textareas = element.querySelectorAll?.('textarea') if (textareas) { for (const textarea of textareas) { - initializeMaybeIsPageload(textarea) + enhanceMaybe(textarea) } } } @@ -47,13 +47,13 @@ function handleMutations(mutations: MutationRecord[]): void { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as Element if (element.tagName === 'TEXTAREA') { - textareaRegistry.unregisterDueToModification(element as HTMLTextAreaElement) + enhancedTextareas.unregisterDueToModification(element as HTMLTextAreaElement) } else { // Also check for textareas within removed subtrees const textareas = element.querySelectorAll?.('textarea') if (textareas) { for (const textarea of textareas) { - textareaRegistry.unregisterDueToModification(textarea) + enhancedTextareas.unregisterDueToModification(textarea) } } } @@ -62,9 +62,9 @@ function handleMutations(mutations: MutationRecord[]): void { } } -function initializeMaybeIsPageload(textarea: HTMLTextAreaElement) { +function enhanceMaybe(textarea: HTMLTextAreaElement) { // Check if this textarea is already registered - if (textareaRegistry.get(textarea)) { + if (enhancedTextareas.get(textarea)) { logger.debug('textarea already registered {}', textarea) return } @@ -73,10 +73,14 @@ function initializeMaybeIsPageload(textarea: HTMLTextAreaElement) { injectStyles() // Use registry to identify and handle this specific textarea - const textareaInfo = handlerRegistry.identifyTextarea(textarea) - if (textareaInfo) { - logger.debug('Identified textarea:', textareaInfo.context.type, textareaInfo.context.unique_key) - textareaRegistry.register(textareaInfo) + const enhancedTextarea = enhancers.identifyTextarea(textarea) + if (enhancedTextarea) { + logger.debug( + 'Identified textarea:', + enhancedTextarea.context.type, + enhancedTextarea.context.unique_key, + ) + enhancedTextareas.register(enhancedTextarea) } else { logger.debug('No handler found for textarea') } From bdd4662850bbcc6cca42cd7e1386758b69c3eca8 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 08:32:49 -0700 Subject: [PATCH 22/54] Update README with progress so far. --- browser-extension/README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/browser-extension/README.md b/browser-extension/README.md index 8c1dab6..ec8b7bb 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -27,10 +27,18 @@ This is a [WXT](https://wxt.dev/)-based browser extension that -- finds `textarea` components and decorates them with [overtype](https://overtype.dev/) and [shiki](https://github.com/shikijs/shiki). +- finds `textarea` components and decorates them with [overtype](https://overtype.dev/) and [highlightjs](https://highlightjs.org/) - stores unposted comment drafts, and makes them easy to find via the extension popup ### Entry points -- src/entrypoints/content.ts - injected into every webpage -- src/entrypoints/popup - html/css/ts which opens when the extension's button gets clicked +- `src/entrypoints/content.ts` - injected into every webpage +- `src/entrypoints/popup` - html/css/ts which opens when the extension's button gets clicked + +### 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`]. + +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). From 4d0ad6a04ec69732e2b926d589705e04c85ddb30 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 08:56:29 -0700 Subject: [PATCH 23/54] Add a mock `Overtype` while we wait to merge. --- .../src/overtype/mock-overtype.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 browser-extension/src/overtype/mock-overtype.ts diff --git a/browser-extension/src/overtype/mock-overtype.ts b/browser-extension/src/overtype/mock-overtype.ts new file mode 100644 index 0000000..fec648c --- /dev/null +++ b/browser-extension/src/overtype/mock-overtype.ts @@ -0,0 +1,44 @@ +/** + * Mock implementation of Overtype for development + * This wraps a textarea and provides a minimal interface + */ +export class OverType { + public element: HTMLTextAreaElement + public instanceId: number + public initialized: boolean = true + + private static instanceCount = 0 + + constructor(target: HTMLTextAreaElement) { + this.element = target + this.instanceId = ++OverType.instanceCount + + // Store reference on the element + ;(target as any).overTypeInstance = this + + // Apply basic styling or enhancement + this.enhance() + } + + private enhance(): void { + // Mock enhancement - could add basic styling, event handlers, etc. + this.element.style.fontFamily = + 'Monaco, "Cascadia Code", "Roboto Mono", Consolas, "Courier New", monospace' + this.element.style.fontSize = '14px' + this.element.style.lineHeight = '1.5' + } + + getValue(): string { + return this.element.value + } + + setValue(value: string): void { + this.element.value = value + } + + destroy(): void { + // Clean up any enhancements + delete (this.element as any).overTypeInstance + this.initialized = false + } +} From faef782eb7a06da7da5f7ee8dc82b1cdff69fe5e Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 08:58:03 -0700 Subject: [PATCH 24/54] The enhancers now create the `Overtype` (as they should). --- browser-extension/src/datamodel/enhancer.ts | 4 +++- .../src/datamodel/handlers/github-handler.ts | 8 ++++++-- .../src/datamodel/handlers/reddit-handler.ts | 8 ++++++-- browser-extension/src/datamodel/registries.ts | 9 ++++++--- browser-extension/tsconfig.json | 2 +- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/browser-extension/src/datamodel/enhancer.ts b/browser-extension/src/datamodel/enhancer.ts index 3809bf4..d5c4c31 100644 --- a/browser-extension/src/datamodel/enhancer.ts +++ b/browser-extension/src/datamodel/enhancer.ts @@ -1,3 +1,5 @@ +import type { OverType } from '../overtype/mock-overtype' + /** * stores enough info about the location of a draft to: * - display it in a table @@ -16,7 +18,7 @@ export interface CommentEnhancer { * whenever a new `textarea` is added to any webpage, this method is called. * if we return non-null, then we become the handler for that text area. */ - identifyContextOf(textarea: HTMLTextAreaElement): T | null + tryToEnhance(textarea: HTMLTextAreaElement): [OverType, T] | null generateIcon(context: T): string generateDisplayTitle(context: T): string diff --git a/browser-extension/src/datamodel/handlers/github-handler.ts b/browser-extension/src/datamodel/handlers/github-handler.ts index 4ff6795..5205e1f 100644 --- a/browser-extension/src/datamodel/handlers/github-handler.ts +++ b/browser-extension/src/datamodel/handlers/github-handler.ts @@ -1,3 +1,4 @@ +import { OverType } from '../../overtype/mock-overtype' import type { CommentEnhancer, CommentSpot } from '../enhancer' export type GitHubCommentType = @@ -30,7 +31,7 @@ export class GitHubHandler implements CommentEnhancer { ] } - identifyContextOf(textarea: HTMLTextAreaElement): GitHubContext | null { + tryToEnhance(textarea: HTMLTextAreaElement): [OverType, GitHubContext] | null { // Only handle GitHub domains if (!window.location.hostname.includes('github')) { return null @@ -103,7 +104,10 @@ export class GitHubHandler implements CommentEnhancer { unique_key, } - return context + // Create OverType instance for this textarea + const overtype = new OverType(textarea) + + return [overtype, context] } generateDisplayTitle(context: GitHubContext): string { diff --git a/browser-extension/src/datamodel/handlers/reddit-handler.ts b/browser-extension/src/datamodel/handlers/reddit-handler.ts index d7f677b..fe57256 100644 --- a/browser-extension/src/datamodel/handlers/reddit-handler.ts +++ b/browser-extension/src/datamodel/handlers/reddit-handler.ts @@ -1,3 +1,4 @@ +import { OverType } from '../../overtype/mock-overtype' import type { CommentEnhancer, CommentSpot } from '../enhancer' export type RedditCommentType = 'REDDIT_POST_NEW' | 'REDDIT_COMMENT_NEW' | 'REDDIT_COMMENT_EDIT' @@ -14,7 +15,7 @@ export class RedditHandler implements CommentEnhancer { return ['REDDIT_POST_NEW', 'REDDIT_COMMENT_NEW', 'REDDIT_COMMENT_EDIT'] } - identifyContextOf(textarea: HTMLTextAreaElement): RedditContext | null { + tryToEnhance(textarea: HTMLTextAreaElement): [OverType, RedditContext] | null { // Only handle Reddit domains if (!window.location.hostname.includes('reddit')) { return null @@ -80,7 +81,10 @@ export class RedditHandler implements CommentEnhancer { unique_key, } - return context + // Create OverType instance for this textarea + const overtype = new OverType(textarea) + + return [overtype, context] } generateDisplayTitle(context: RedditContext): string { diff --git a/browser-extension/src/datamodel/registries.ts b/browser-extension/src/datamodel/registries.ts index 7200995..f080c13 100644 --- a/browser-extension/src/datamodel/registries.ts +++ b/browser-extension/src/datamodel/registries.ts @@ -1,3 +1,4 @@ +import type { OverType } from '../overtype/mock-overtype' import type { CommentEnhancer, CommentSpot } from './enhancer' import { GitHubHandler as GitHubEnhancer } from './handlers/github-handler' import { RedditHandler as RedditEnhancer } from './handlers/reddit-handler' @@ -6,6 +7,7 @@ export interface EnhancedTextarea { element: HTMLTextAreaElement context: T handler: CommentEnhancer + overtype: OverType } export class EnhancerRegistry { @@ -33,9 +35,10 @@ export class EnhancerRegistry { identifyTextarea(textarea: HTMLTextAreaElement): EnhancedTextarea | null { for (const handler of this.enhancers) { try { - const context = handler.identifyContextOf(textarea) - if (context) { - return { context, element: textarea, handler } + const result = handler.tryToEnhance(textarea) + if (result) { + const [overtype, context] = result + return { context, element: textarea, handler, overtype } } } catch (error) { console.warn('Handler failed to identify textarea:', error) diff --git a/browser-extension/tsconfig.json b/browser-extension/tsconfig.json index a914c1f..90169d5 100644 --- a/browser-extension/tsconfig.json +++ b/browser-extension/tsconfig.json @@ -42,5 +42,5 @@ }, "exclude": ["node_modules", ".output", "dist"], "extends": "./.wxt/tsconfig.json", - "include": ["src/entrypoints/**/*", "*.config.ts", ".wxt/wxt.d.ts", "tests/**/*"] + "include": ["src/**/*", "*.config.ts", ".wxt/wxt.d.ts", "tests/**/*"] } From 220b8354978c10aef41da715e2f62673eb95cd0f Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 09:06:20 -0700 Subject: [PATCH 25/54] Add CI. --- .github/workflows/browser-extension.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/browser-extension.yml diff --git a/.github/workflows/browser-extension.yml b/.github/workflows/browser-extension.yml new file mode 100644 index 0000000..df39e32 --- /dev/null +++ b/.github/workflows/browser-extension.yml @@ -0,0 +1,23 @@ +on: + push: + paths: + - 'browser-extension/**' + pull_request: + paths: + - 'browser-extension/**' +jobs: + lint-and-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version-file: '.nvmrc' + cache: 'npm' + cache-dependency-path: browser-extension/package-lock.json + - run: npm ci + working-directory: browser-extension + - run: npm run biome + working-directory: browser-extension + - run: npm run compile + working-directory: browser-extension \ No newline at end of file From 0805bda3090dd5d1f40a7e7da0506013c0129201 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 09:21:59 -0700 Subject: [PATCH 26/54] A bit of consistency. --- browser-extension/src/datamodel/registries.ts | 2 +- browser-extension/src/entrypoints/content.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/browser-extension/src/datamodel/registries.ts b/browser-extension/src/datamodel/registries.ts index f080c13..782052b 100644 --- a/browser-extension/src/datamodel/registries.ts +++ b/browser-extension/src/datamodel/registries.ts @@ -32,7 +32,7 @@ export class EnhancerRegistry { return null } - identifyTextarea(textarea: HTMLTextAreaElement): EnhancedTextarea | null { + tryToEnhance(textarea: HTMLTextAreaElement): EnhancedTextarea | null { for (const handler of this.enhancers) { try { const result = handler.tryToEnhance(textarea) diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 2716229..858b65c 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -73,7 +73,7 @@ function enhanceMaybe(textarea: HTMLTextAreaElement) { injectStyles() // Use registry to identify and handle this specific textarea - const enhancedTextarea = enhancers.identifyTextarea(textarea) + const enhancedTextarea = enhancers.tryToEnhance(textarea) if (enhancedTextarea) { logger.debug( 'Identified textarea:', From 728dbc0b8a26b16ce2733cfc601db570468d5576 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 09:30:33 -0700 Subject: [PATCH 27/54] Rename. --- browser-extension/src/datamodel/registries.ts | 6 +++--- browser-extension/src/entrypoints/content.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/browser-extension/src/datamodel/registries.ts b/browser-extension/src/datamodel/registries.ts index 782052b..35fc4fc 100644 --- a/browser-extension/src/datamodel/registries.ts +++ b/browser-extension/src/datamodel/registries.ts @@ -5,7 +5,7 @@ import { RedditHandler as RedditEnhancer } from './handlers/reddit-handler' export interface EnhancedTextarea { element: HTMLTextAreaElement - context: T + spot: T handler: CommentEnhancer overtype: OverType } @@ -37,8 +37,8 @@ export class EnhancerRegistry { try { const result = handler.tryToEnhance(textarea) if (result) { - const [overtype, context] = result - return { context, element: textarea, handler, overtype } + const [overtype, spot] = result + return { element: textarea, handler, overtype, spot } } } catch (error) { console.warn('Handler failed to identify textarea:', error) diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 858b65c..14feab2 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -77,8 +77,8 @@ function enhanceMaybe(textarea: HTMLTextAreaElement) { if (enhancedTextarea) { logger.debug( 'Identified textarea:', - enhancedTextarea.context.type, - enhancedTextarea.context.unique_key, + enhancedTextarea.spot.type, + enhancedTextarea.spot.unique_key, ) enhancedTextareas.register(enhancedTextarea) } else { From bfe01769b67bcc210795b068e66feb75c9e06ffe Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 09:36:29 -0700 Subject: [PATCH 28/54] Use WXT's testing harness, don't reinvent the wheel. --- browser-extension/tests/setup.ts | 37 ------------------------------ browser-extension/vitest.config.ts | 10 ++------ 2 files changed, 2 insertions(+), 45 deletions(-) diff --git a/browser-extension/tests/setup.ts b/browser-extension/tests/setup.ts index 1171022..a9d0dd3 100644 --- a/browser-extension/tests/setup.ts +++ b/browser-extension/tests/setup.ts @@ -1,38 +1 @@ import '@testing-library/jest-dom/vitest' -import { vi } from 'vitest' - -// Mock browser APIs -interface GlobalWithBrowser { - browser: { - runtime: { - sendMessage: ReturnType - onMessage: { - addListener: ReturnType - } - } - } - chrome: GlobalWithBrowser['browser'] -} - -const globalWithBrowser = global as unknown as GlobalWithBrowser - -globalWithBrowser.browser = { - runtime: { - onMessage: { - addListener: vi.fn(), - }, - sendMessage: vi.fn(), - }, -} - -globalWithBrowser.chrome = globalWithBrowser.browser - -// Mock console methods to reduce noise in tests -global.console = { - ...console, - debug: vi.fn(), - error: vi.fn(), - info: vi.fn(), - log: vi.fn(), - warn: vi.fn(), -} diff --git a/browser-extension/vitest.config.ts b/browser-extension/vitest.config.ts index d247fc3..af7d365 100644 --- a/browser-extension/vitest.config.ts +++ b/browser-extension/vitest.config.ts @@ -1,14 +1,8 @@ -import path from 'node:path' import { defineConfig } from 'vitest/config' +import { WxtVitest } from 'wxt/testing' export default defineConfig({ - resolve: { - alias: { - '@': path.resolve(__dirname, './entrypoints'), - '@content': path.resolve(__dirname, './entrypoints/content'), - '@utils': path.resolve(__dirname, './entrypoints/content/utils'), - }, - }, + plugins: [WxtVitest()], test: { coverage: { exclude: [ From ed34d42b8526aec0d6f026b8e650d1067fa606c2 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 09:39:45 -0700 Subject: [PATCH 29/54] First cut at a `content.test.ts` --- .../__snapshots__/content.test.ts.snap | 12 ++ .../src/entrypoints/content.test.ts | 119 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 browser-extension/src/entrypoints/__snapshots__/content.test.ts.snap create mode 100644 browser-extension/src/entrypoints/content.test.ts diff --git a/browser-extension/src/entrypoints/__snapshots__/content.test.ts.snap b/browser-extension/src/entrypoints/__snapshots__/content.test.ts.snap new file mode 100644 index 0000000..3ffa7ac --- /dev/null +++ b/browser-extension/src/entrypoints/__snapshots__/content.test.ts.snap @@ -0,0 +1,12 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`GitHub PR Content Script > should create correct GitHubContext spot for PR comment > github-pr-517-spot 1`] = ` +{ + "commentId": undefined, + "domain": "github.com", + "number": 517, + "slug": "diffplug/selfie", + "type": "GH_PR_ADD_COMMENT", + "unique_key": "github:diffplug/selfie:pull:517", +} +`; diff --git a/browser-extension/src/entrypoints/content.test.ts b/browser-extension/src/entrypoints/content.test.ts new file mode 100644 index 0000000..02a24a6 --- /dev/null +++ b/browser-extension/src/entrypoints/content.test.ts @@ -0,0 +1,119 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { EnhancerRegistry, TextareaRegistry } from '../datamodel/registries' + +// Mock WXT's defineContentScript global +vi.stubGlobal('defineContentScript', vi.fn()) + +describe('GitHub PR Content Script', () => { + let enhancers: EnhancerRegistry + let enhancedTextareas: TextareaRegistry + let mockTextarea: HTMLTextAreaElement + + beforeEach(() => { + // Reset DOM and registries for each test + document.body.innerHTML = '' + enhancers = new EnhancerRegistry() + enhancedTextareas = new TextareaRegistry() + + // Mock window.location for GitHub PR page + Object.defineProperty(window, 'location', { + writable: true, + value: { + hostname: 'github.com', + pathname: '/diffplug/selfie/pull/517', + href: 'https://github.com/diffplug/selfie/pull/517' + } + }) + + // Create a mock textarea element that mimics GitHub's PR comment box + mockTextarea = document.createElement('textarea') + mockTextarea.name = 'comment[body]' + mockTextarea.placeholder = 'Leave a comment' + mockTextarea.className = 'form-control markdown-body' + + // Add it to a typical GitHub comment form structure + const commentForm = document.createElement('div') + commentForm.className = 'js-new-comment-form' + commentForm.appendChild(mockTextarea) + document.body.appendChild(commentForm) + }) + + it('should identify GitHub PR textarea and register it in TextareaRegistry', () => { + // Simulate the content script's enhanceMaybe function + const enhancedTextarea = enhancers.tryToEnhance(mockTextarea) + + expect(enhancedTextarea).toBeTruthy() + expect(enhancedTextarea?.element).toBe(mockTextarea) + expect(enhancedTextarea?.spot.type).toBe('GH_PR_ADD_COMMENT') + + // Register the enhanced textarea + if (enhancedTextarea) { + enhancedTextareas.register(enhancedTextarea) + } + + // Verify it's in the registry + const registeredTextarea = enhancedTextareas.get(mockTextarea) + expect(registeredTextarea).toBeTruthy() + expect(registeredTextarea?.element).toBe(mockTextarea) + }) + + it('should create correct GitHubContext spot for PR comment', () => { + const enhancedTextarea = enhancers.tryToEnhance(mockTextarea) + + expect(enhancedTextarea).toBeTruthy() + + // Snapshot test on the spot value + expect(enhancedTextarea?.spot).toMatchSnapshot('github-pr-517-spot') + + // Also verify specific expected values + expect(enhancedTextarea?.spot).toMatchObject({ + type: 'GH_PR_ADD_COMMENT', + domain: 'github.com', + slug: 'diffplug/selfie', + number: 517, + unique_key: 'github:diffplug/selfie:pull:517', + commentId: undefined + }) + }) + + it('should handle multiple textareas on the same page', () => { + // Create a second textarea for inline code comments + const codeCommentTextarea = document.createElement('textarea') + codeCommentTextarea.className = 'form-control js-suggester-field' + + const inlineForm = document.createElement('div') + inlineForm.className = 'js-inline-comment-form' + inlineForm.appendChild(codeCommentTextarea) + document.body.appendChild(inlineForm) + + // Test both textareas + const mainCommentEnhanced = enhancers.tryToEnhance(mockTextarea) + const codeCommentEnhanced = enhancers.tryToEnhance(codeCommentTextarea) + + expect(mainCommentEnhanced?.spot.type).toBe('GH_PR_ADD_COMMENT') + expect(codeCommentEnhanced?.spot.type).toBe('GH_PR_CODE_COMMENT') + + // Register both + if (mainCommentEnhanced) enhancedTextareas.register(mainCommentEnhanced) + if (codeCommentEnhanced) enhancedTextareas.register(codeCommentEnhanced) + + // Verify both are registered + expect(enhancedTextareas.get(mockTextarea)).toBeTruthy() + expect(enhancedTextareas.get(codeCommentTextarea)).toBeTruthy() + }) + + it('should not enhance textarea on non-GitHub pages', () => { + // Change location to non-GitHub site + Object.defineProperty(window, 'location', { + writable: true, + value: { + hostname: 'example.com', + pathname: '/some/page', + href: 'https://example.com/some/page' + } + }) + + const enhancedTextarea = enhancers.tryToEnhance(mockTextarea) + expect(enhancedTextarea).toBeNull() + }) +}) \ No newline at end of file From dfee834c7106a596e938f02d060859e7266a3ec3 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 09:51:53 -0700 Subject: [PATCH 30/54] Move the test to a better place. --- .../handlers/__snapshots__/github-handler.test.ts.snap} | 2 +- .../datamodel/handlers/github-handler.test.ts} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename browser-extension/{src/entrypoints/__snapshots__/content.test.ts.snap => tests/datamodel/handlers/__snapshots__/github-handler.test.ts.snap} (67%) rename browser-extension/{src/entrypoints/content.test.ts => tests/datamodel/handlers/github-handler.test.ts} (97%) diff --git a/browser-extension/src/entrypoints/__snapshots__/content.test.ts.snap b/browser-extension/tests/datamodel/handlers/__snapshots__/github-handler.test.ts.snap similarity index 67% rename from browser-extension/src/entrypoints/__snapshots__/content.test.ts.snap rename to browser-extension/tests/datamodel/handlers/__snapshots__/github-handler.test.ts.snap index 3ffa7ac..56852e7 100644 --- a/browser-extension/src/entrypoints/__snapshots__/content.test.ts.snap +++ b/browser-extension/tests/datamodel/handlers/__snapshots__/github-handler.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`GitHub PR Content Script > should create correct GitHubContext spot for PR comment > github-pr-517-spot 1`] = ` +exports[`GitHubHandler > should create correct GitHubContext spot for PR comment > github-pr-517-spot 1`] = ` { "commentId": undefined, "domain": "github.com", diff --git a/browser-extension/src/entrypoints/content.test.ts b/browser-extension/tests/datamodel/handlers/github-handler.test.ts similarity index 97% rename from browser-extension/src/entrypoints/content.test.ts rename to browser-extension/tests/datamodel/handlers/github-handler.test.ts index 02a24a6..24e35a0 100644 --- a/browser-extension/src/entrypoints/content.test.ts +++ b/browser-extension/tests/datamodel/handlers/github-handler.test.ts @@ -1,10 +1,10 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { EnhancerRegistry, TextareaRegistry } from '../datamodel/registries' +import { EnhancerRegistry, TextareaRegistry } from '../../../src/datamodel/registries' // Mock WXT's defineContentScript global vi.stubGlobal('defineContentScript', vi.fn()) -describe('GitHub PR Content Script', () => { +describe('GitHubHandler', () => { let enhancers: EnhancerRegistry let enhancedTextareas: TextareaRegistry let mockTextarea: HTMLTextAreaElement From cfdea3fd485ce6e5eaeda7ad7bab3d4fa1d6e79a Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 09:52:48 -0700 Subject: [PATCH 31/54] Run tests in CI. --- .github/workflows/browser-extension.yml | 4 +++- browser-extension/README.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/browser-extension.yml b/.github/workflows/browser-extension.yml index df39e32..de2db7f 100644 --- a/.github/workflows/browser-extension.yml +++ b/.github/workflows/browser-extension.yml @@ -6,7 +6,7 @@ on: paths: - 'browser-extension/**' jobs: - lint-and-build: + lint-test-and-build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -19,5 +19,7 @@ jobs: working-directory: browser-extension - run: npm run biome working-directory: browser-extension + - run: npm test + working-directory: browser-extension - run: npm run compile working-directory: browser-extension \ No newline at end of file diff --git a/browser-extension/README.md b/browser-extension/README.md index ec8b7bb..8eed095 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -17,7 +17,7 @@ - `npm run biome` - runs `biome check` (lint & formatting) - `npm run biome:fix` - fixes most of what `biome check` finds - `npm run compile` - typechecking -- `npm run test` - vitest +- `npm test` - vitest ### Deployment - `npm run build` - build for mv3 for most browsers From fe8ae0955ad771968b2e259ac3daaefed302398f Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 09:54:58 -0700 Subject: [PATCH 32/54] Be more efficient with CI resources. --- .github/workflows/browser-extension.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/browser-extension.yml b/.github/workflows/browser-extension.yml index de2db7f..a547efa 100644 --- a/.github/workflows/browser-extension.yml +++ b/.github/workflows/browser-extension.yml @@ -1,10 +1,11 @@ on: - push: - paths: - - 'browser-extension/**' pull_request: - paths: - - 'browser-extension/**' + push: + branches: [main, release] + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: lint-test-and-build: runs-on: ubuntu-latest From 2561ef7f3ab972a13915b9702d71cd52c52d67a2 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 09:55:15 -0700 Subject: [PATCH 33/54] biome the test --- .../datamodel/handlers/github-handler.test.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/browser-extension/tests/datamodel/handlers/github-handler.test.ts b/browser-extension/tests/datamodel/handlers/github-handler.test.ts index 24e35a0..0946a87 100644 --- a/browser-extension/tests/datamodel/handlers/github-handler.test.ts +++ b/browser-extension/tests/datamodel/handlers/github-handler.test.ts @@ -17,12 +17,12 @@ describe('GitHubHandler', () => { // Mock window.location for GitHub PR page Object.defineProperty(window, 'location', { - writable: true, value: { hostname: 'github.com', + href: 'https://github.com/diffplug/selfie/pull/517', pathname: '/diffplug/selfie/pull/517', - href: 'https://github.com/diffplug/selfie/pull/517' - } + }, + writable: true, }) // Create a mock textarea element that mimics GitHub's PR comment box @@ -30,7 +30,7 @@ describe('GitHubHandler', () => { mockTextarea.name = 'comment[body]' mockTextarea.placeholder = 'Leave a comment' mockTextarea.className = 'form-control markdown-body' - + // Add it to a typical GitHub comment form structure const commentForm = document.createElement('div') commentForm.className = 'js-new-comment-form' @@ -41,16 +41,16 @@ describe('GitHubHandler', () => { it('should identify GitHub PR textarea and register it in TextareaRegistry', () => { // Simulate the content script's enhanceMaybe function const enhancedTextarea = enhancers.tryToEnhance(mockTextarea) - + expect(enhancedTextarea).toBeTruthy() expect(enhancedTextarea?.element).toBe(mockTextarea) expect(enhancedTextarea?.spot.type).toBe('GH_PR_ADD_COMMENT') - + // Register the enhanced textarea if (enhancedTextarea) { enhancedTextareas.register(enhancedTextarea) } - + // Verify it's in the registry const registeredTextarea = enhancedTextareas.get(mockTextarea) expect(registeredTextarea).toBeTruthy() @@ -59,20 +59,20 @@ describe('GitHubHandler', () => { it('should create correct GitHubContext spot for PR comment', () => { const enhancedTextarea = enhancers.tryToEnhance(mockTextarea) - + expect(enhancedTextarea).toBeTruthy() - + // Snapshot test on the spot value expect(enhancedTextarea?.spot).toMatchSnapshot('github-pr-517-spot') - + // Also verify specific expected values expect(enhancedTextarea?.spot).toMatchObject({ - type: 'GH_PR_ADD_COMMENT', + commentId: undefined, domain: 'github.com', - slug: 'diffplug/selfie', number: 517, + slug: 'diffplug/selfie', + type: 'GH_PR_ADD_COMMENT', unique_key: 'github:diffplug/selfie:pull:517', - commentId: undefined }) }) @@ -80,7 +80,7 @@ describe('GitHubHandler', () => { // Create a second textarea for inline code comments const codeCommentTextarea = document.createElement('textarea') codeCommentTextarea.className = 'form-control js-suggester-field' - + const inlineForm = document.createElement('div') inlineForm.className = 'js-inline-comment-form' inlineForm.appendChild(codeCommentTextarea) @@ -92,7 +92,7 @@ describe('GitHubHandler', () => { expect(mainCommentEnhanced?.spot.type).toBe('GH_PR_ADD_COMMENT') expect(codeCommentEnhanced?.spot.type).toBe('GH_PR_CODE_COMMENT') - + // Register both if (mainCommentEnhanced) enhancedTextareas.register(mainCommentEnhanced) if (codeCommentEnhanced) enhancedTextareas.register(codeCommentEnhanced) @@ -105,15 +105,15 @@ describe('GitHubHandler', () => { it('should not enhance textarea on non-GitHub pages', () => { // Change location to non-GitHub site Object.defineProperty(window, 'location', { - writable: true, value: { hostname: 'example.com', + href: 'https://example.com/some/page', pathname: '/some/page', - href: 'https://example.com/some/page' - } + }, + writable: true, }) const enhancedTextarea = enhancers.tryToEnhance(mockTextarea) expect(enhancedTextarea).toBeNull() }) -}) \ No newline at end of file +}) From afe4b0dd2036e83ad96713624f1dd3ce9b0098f5 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 10:38:07 -0700 Subject: [PATCH 34/54] Minor tagline and version fixes. --- README.md | 6 +++--- browser-extension/package.json | 4 ++-- browser-extension/src/entrypoints/popup/index.html | 2 +- browser-extension/wxt.config.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f46d8ba..3204ae5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Gitcasso -*Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly places).* +*Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).* - "Syntax highlighting is the lie that enables us to see the truth." - "The meaning of life is to find your lost comment drafts. The purpose of life is to post them." @@ -12,6 +12,6 @@ TODO: screenshot of comment draft storage and restoration If there's something you'd like to add or fix, see [CONTRIBUTING.md](CONTRIBUTING.md). Special thanks to: -- [overtype](https://github.com/panphora/overtype) for the trick which makes syntax highlighting possible -- [shiki](https://github.com/shikijs/shiki) for the broad library of syntax highlighters +- [overtype](https://overtype.dev/) for doing `textarea` syntax highlighting of `md` +- [highlight.js](https://highlightjs.org/) for the broad library of syntax highlighters - [Yukai Huang](https://github.com/Yukaii) for [the PRs](https://github.com/panphora/overtype/issues?q=is%3Apr+author%3AYukaii) which made the two work together \ No newline at end of file diff --git a/browser-extension/package.json b/browser-extension/package.json index b9413cb..d79cdb5 100644 --- a/browser-extension/package.json +++ b/browser-extension/package.json @@ -4,7 +4,7 @@ "@wxt-dev/webextension-polyfill": "^1.0.0", "webextension-polyfill": "^0.12.0" }, - "description": "Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly places).", + "description": "Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).", "devDependencies": { "@biomejs/biome": "^2.1.2", "@testing-library/jest-dom": "^6.6.4", @@ -41,5 +41,5 @@ "zip:firefox": "wxt zip -b firefox" }, "type": "module", - "version": "1.0.0" + "version": "0.0.1" } diff --git a/browser-extension/src/entrypoints/popup/index.html b/browser-extension/src/entrypoints/popup/index.html index 650ae14..a9c17b6 100644 --- a/browser-extension/src/entrypoints/popup/index.html +++ b/browser-extension/src/entrypoints/popup/index.html @@ -9,7 +9,7 @@
-
Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly places).
+
Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).

Loading drafts from local storage...

diff --git a/browser-extension/wxt.config.ts b/browser-extension/wxt.config.ts index 0514d1a..edc2589 100644 --- a/browser-extension/wxt.config.ts +++ b/browser-extension/wxt.config.ts @@ -3,7 +3,7 @@ import { defineConfig } from 'wxt' export default defineConfig({ manifest: { description: - 'Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly places).', + 'Syntax highlighting and autosave for comments on GitHub (and other other markdown-friendly websites).', host_permissions: ['https://*/*', 'http://*/*'], icons: { 16: '/icons/icon-16.png', From 5875110e226291400a3ad51bd4eeb9390c5740be Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 10:42:59 -0700 Subject: [PATCH 35/54] Remove `RedditEnhancer`, we'll get there eventually... --- .../src/datamodel/handlers/reddit-handler.ts | 143 ------------------ browser-extension/src/datamodel/registries.ts | 2 - 2 files changed, 145 deletions(-) delete mode 100644 browser-extension/src/datamodel/handlers/reddit-handler.ts diff --git a/browser-extension/src/datamodel/handlers/reddit-handler.ts b/browser-extension/src/datamodel/handlers/reddit-handler.ts deleted file mode 100644 index fe57256..0000000 --- a/browser-extension/src/datamodel/handlers/reddit-handler.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { OverType } from '../../overtype/mock-overtype' -import type { CommentEnhancer, CommentSpot } from '../enhancer' - -export type RedditCommentType = 'REDDIT_POST_NEW' | 'REDDIT_COMMENT_NEW' | 'REDDIT_COMMENT_EDIT' - -export interface RedditContext extends CommentSpot { - type: RedditCommentType // Override to narrow from string to specific union - subreddit: string - postId?: string | undefined - commentId?: string | undefined // for editing existing comments -} - -export class RedditHandler implements CommentEnhancer { - forCommentTypes(): string[] { - return ['REDDIT_POST_NEW', 'REDDIT_COMMENT_NEW', 'REDDIT_COMMENT_EDIT'] - } - - tryToEnhance(textarea: HTMLTextAreaElement): [OverType, RedditContext] | null { - // Only handle Reddit domains - if (!window.location.hostname.includes('reddit')) { - return null - } - - const pathname = window.location.pathname - - // Parse Reddit URL structure: /r/subreddit/comments/postid/title/ - const postMatch = pathname.match(/^\/r\/([^/]+)\/comments\/([^/]+)/) - const submitMatch = pathname.match(/^\/r\/([^/]+)\/submit/) - const subredditMatch = pathname.match(/^\/r\/([^/]+)/) - - let subreddit: string | undefined - let postId: string | undefined - - if (postMatch) { - ;[, subreddit, postId] = postMatch - } else if (submitMatch) { - ;[, subreddit] = submitMatch - } else if (subredditMatch) { - ;[, subreddit] = subredditMatch - } - - if (!subreddit) { - return null - } - - // Check if editing existing comment - const commentId = this.getCommentId(textarea) - - // Determine comment type - let type: RedditCommentType - - // New post submission - if (pathname.includes('/submit')) { - type = 'REDDIT_POST_NEW' - } - // Check if we're on a post page - else if (pathname.match(/\/r\/[^/]+\/comments\/[^/]+/)) { - const isEditingComment = commentId !== null - type = isEditingComment ? 'REDDIT_COMMENT_EDIT' : 'REDDIT_COMMENT_NEW' - } else { - return null - } - - // Generate unique key - let unique_key = `reddit:${subreddit}` - if (postId) { - unique_key += `:${postId}` - } else { - unique_key += ':new' - } - - if (commentId) { - unique_key += `:edit:${commentId}` - } - - const context: RedditContext = { - commentId: commentId || undefined, - postId, - subreddit, - type, - unique_key, - } - - // Create OverType instance for this textarea - const overtype = new OverType(textarea) - - return [overtype, context] - } - - generateDisplayTitle(context: RedditContext): string { - const { subreddit, postId, commentId } = context - - if (commentId) { - return `Edit comment in r/${subreddit}` - } - - if (postId) { - return `Comment in r/${subreddit}` - } - - return `New post in r/${subreddit}` - } - - generateIcon(context: RedditContext): string { - switch (context.type) { - case 'REDDIT_POST_NEW': - return '📝' // Post icon - case 'REDDIT_COMMENT_NEW': - return '💬' // Comment icon - case 'REDDIT_COMMENT_EDIT': - return '✏️' // Edit icon - default: - return '🔵' // Reddit icon - } - } - - buildUrl(context: RedditContext): string { - const baseUrl = `https://reddit.com/r/${context.subreddit}` - - if (context.postId) { - return `${baseUrl}/comments/${context.postId}/${context.commentId ? `#${context.commentId}` : ''}` - } - - return baseUrl - } - - private getCommentId(textarea: HTMLTextAreaElement): string | null { - // Look for edit comment form indicators - const commentForm = textarea.closest('[data-comment-id]') - if (commentForm) { - return commentForm.getAttribute('data-comment-id') - } - - // Reddit uses different class names, check for common edit form patterns - const editForm = textarea.closest('.edit-usertext') || textarea.closest('[data-type="comment"]') - if (editForm) { - const id = editForm.getAttribute('data-fullname') || editForm.getAttribute('data-comment-id') - return id - } - - return null - } -} diff --git a/browser-extension/src/datamodel/registries.ts b/browser-extension/src/datamodel/registries.ts index 35fc4fc..27e5f3d 100644 --- a/browser-extension/src/datamodel/registries.ts +++ b/browser-extension/src/datamodel/registries.ts @@ -1,7 +1,6 @@ import type { OverType } from '../overtype/mock-overtype' import type { CommentEnhancer, CommentSpot } from './enhancer' import { GitHubHandler as GitHubEnhancer } from './handlers/github-handler' -import { RedditHandler as RedditEnhancer } from './handlers/reddit-handler' export interface EnhancedTextarea { element: HTMLTextAreaElement @@ -16,7 +15,6 @@ export class EnhancerRegistry { constructor() { // Register all available handlers this.register(new GitHubEnhancer()) - this.register(new RedditEnhancer()) } private register(handler: CommentEnhancer): void { From fe74e99cf5301ee8706eeaa53efc9af3493c01cb Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 10:47:30 -0700 Subject: [PATCH 36/54] Use typescript features a little better. --- .../src/datamodel/handlers/github-handler.ts | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/browser-extension/src/datamodel/handlers/github-handler.ts b/browser-extension/src/datamodel/handlers/github-handler.ts index 5205e1f..a467a88 100644 --- a/browser-extension/src/datamodel/handlers/github-handler.ts +++ b/browser-extension/src/datamodel/handlers/github-handler.ts @@ -1,14 +1,17 @@ import { OverType } from '../../overtype/mock-overtype' import type { CommentEnhancer, CommentSpot } from '../enhancer' -export type GitHubCommentType = - | 'GH_ISSUE_NEW' - | 'GH_PR_NEW' - | 'GH_ISSUE_ADD_COMMENT' - | 'GH_ISSUE_EDIT_COMMENT' - | 'GH_PR_ADD_COMMENT' - | 'GH_PR_EDIT_COMMENT' - | 'GH_PR_CODE_COMMENT' +const GITHUB_COMMENT_TYPES = [ + 'GH_ISSUE_NEW', + 'GH_PR_NEW', + 'GH_ISSUE_ADD_COMMENT', + 'GH_ISSUE_EDIT_COMMENT', + 'GH_PR_ADD_COMMENT', + 'GH_PR_EDIT_COMMENT', + 'GH_PR_CODE_COMMENT', +] as const + +export type GitHubCommentType = (typeof GITHUB_COMMENT_TYPES)[number] export interface GitHubContext extends CommentSpot { type: GitHubCommentType // Override to narrow from string to specific union @@ -20,15 +23,7 @@ export interface GitHubContext extends CommentSpot { export class GitHubHandler implements CommentEnhancer { forCommentTypes(): string[] { - return [ - 'GH_ISSUE_NEW', - 'GH_PR_NEW', - 'GH_ISSUE_ADD_COMMENT', - 'GH_ISSUE_EDIT_COMMENT', - 'GH_PR_ADD_COMMENT', - 'GH_PR_EDIT_COMMENT', - 'GH_PR_CODE_COMMENT', - ] + return [...GITHUB_COMMENT_TYPES] } tryToEnhance(textarea: HTMLTextAreaElement): [OverType, GitHubContext] | null { From a7420f471c29f738615a1c0dcbcd765e561c6a67 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 10:55:26 -0700 Subject: [PATCH 37/54] Simplify `github-handler` down to only the things which are likely to make it into v1 --- .../src/datamodel/handlers/github-handler.ts | 64 +++---------------- 1 file changed, 8 insertions(+), 56 deletions(-) diff --git a/browser-extension/src/datamodel/handlers/github-handler.ts b/browser-extension/src/datamodel/handlers/github-handler.ts index a467a88..fdd61be 100644 --- a/browser-extension/src/datamodel/handlers/github-handler.ts +++ b/browser-extension/src/datamodel/handlers/github-handler.ts @@ -5,10 +5,12 @@ const GITHUB_COMMENT_TYPES = [ 'GH_ISSUE_NEW', 'GH_PR_NEW', 'GH_ISSUE_ADD_COMMENT', - 'GH_ISSUE_EDIT_COMMENT', 'GH_PR_ADD_COMMENT', + /* TODO + 'GH_ISSUE_EDIT_COMMENT', 'GH_PR_EDIT_COMMENT', 'GH_PR_CODE_COMMENT', + */ ] as const export type GitHubCommentType = (typeof GITHUB_COMMENT_TYPES)[number] @@ -17,8 +19,7 @@ export interface GitHubContext extends CommentSpot { type: GitHubCommentType // Override to narrow from string to specific union domain: string slug: string // owner/repo - number?: number | undefined // issue/PR number - commentId?: string | undefined // for editing existing comments + number?: number | undefined // issue/PR number, undefined for new issues and PRs } export class GitHubHandler implements CommentEnhancer { @@ -42,9 +43,6 @@ export class GitHubHandler implements CommentEnhancer { const slug = `${owner}/${repo}` const number = numberStr ? parseInt(numberStr, 10) : undefined - // Check if editing existing comment - const commentId = this.getCommentId(textarea) - // Determine comment type let type: GitHubCommentType @@ -58,21 +56,10 @@ export class GitHubHandler implements CommentEnhancer { } // Existing issue or PR page else if (urlType && number) { - const isEditingComment = commentId !== null - if (urlType === 'issues') { - type = isEditingComment ? 'GH_ISSUE_EDIT_COMMENT' : 'GH_ISSUE_ADD_COMMENT' + type = 'GH_ISSUE_ADD_COMMENT' } else { - // Check if it's a code comment (in Files Changed tab) - const isCodeComment = - textarea.closest('.js-inline-comment-form') !== null || - textarea.closest('[data-path]') !== null - - if (isCodeComment) { - type = 'GH_PR_CODE_COMMENT' - } else { - type = isEditingComment ? 'GH_PR_EDIT_COMMENT' : 'GH_PR_ADD_COMMENT' - } + type = 'GH_PR_ADD_COMMENT' } } else { return null @@ -86,12 +73,8 @@ export class GitHubHandler implements CommentEnhancer { unique_key += ':new' } - if (commentId) { - unique_key += `:edit:${commentId}` - } const context: GitHubContext = { - commentId: commentId || undefined, domain: window.location.hostname, number, slug, @@ -106,16 +89,10 @@ export class GitHubHandler implements CommentEnhancer { } generateDisplayTitle(context: GitHubContext): string { - const { slug, number, commentId } = context - - if (commentId) { - return `Edit comment in ${slug}${number ? ` #${number}` : ''}` - } - + const { slug, number } = context if (number) { return `Comment on ${slug} #${number}` } - return `New ${window.location.pathname.includes('/issues/') ? 'issue' : 'PR'} in ${slug}` } @@ -123,16 +100,10 @@ export class GitHubHandler implements CommentEnhancer { switch (context.type) { case 'GH_ISSUE_NEW': case 'GH_ISSUE_ADD_COMMENT': - case 'GH_ISSUE_EDIT_COMMENT': return '🐛' // Issue icon case 'GH_PR_NEW': case 'GH_PR_ADD_COMMENT': - case 'GH_PR_EDIT_COMMENT': return '🔄' // PR icon - case 'GH_PR_CODE_COMMENT': - return '💬' // Code comment icon - default: - return '📝' // Generic comment icon } } @@ -141,28 +112,9 @@ export class GitHubHandler implements CommentEnhancer { if (context.number) { const type = window.location.pathname.includes('/issues/') ? 'issues' : 'pull' - return `${baseUrl}/${type}/${context.number}${context.commentId ? `#issuecomment-${context.commentId}` : ''}` + return `${baseUrl}/${type}/${context.number}` } return baseUrl } - - private getCommentId(textarea: HTMLTextAreaElement): string | null { - // Look for edit comment form indicators - const commentForm = textarea.closest('[data-comment-id]') - if (commentForm) { - return commentForm.getAttribute('data-comment-id') - } - - const editForm = textarea.closest('.js-comment-edit-form') - if (editForm) { - const commentContainer = editForm.closest('.js-comment-container') - if (commentContainer) { - const id = commentContainer.getAttribute('data-gid') || commentContainer.getAttribute('id') - return id ? id.replace('issuecomment-', '') : null - } - } - - return null - } } From 2fb927897117491d0c81f8371f5715cc7a3aa0f6 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 10:59:20 -0700 Subject: [PATCH 38/54] Fixup and simplify tests. --- .../src/datamodel/handlers/github-handler.ts | 1 - .../__snapshots__/github-handler.test.ts.snap | 1 - .../datamodel/handlers/github-handler.test.ts | 27 ------------------- 3 files changed, 29 deletions(-) diff --git a/browser-extension/src/datamodel/handlers/github-handler.ts b/browser-extension/src/datamodel/handlers/github-handler.ts index fdd61be..2b59f4c 100644 --- a/browser-extension/src/datamodel/handlers/github-handler.ts +++ b/browser-extension/src/datamodel/handlers/github-handler.ts @@ -73,7 +73,6 @@ export class GitHubHandler implements CommentEnhancer { unique_key += ':new' } - const context: GitHubContext = { domain: window.location.hostname, number, diff --git a/browser-extension/tests/datamodel/handlers/__snapshots__/github-handler.test.ts.snap b/browser-extension/tests/datamodel/handlers/__snapshots__/github-handler.test.ts.snap index 56852e7..fa2f132 100644 --- a/browser-extension/tests/datamodel/handlers/__snapshots__/github-handler.test.ts.snap +++ b/browser-extension/tests/datamodel/handlers/__snapshots__/github-handler.test.ts.snap @@ -2,7 +2,6 @@ exports[`GitHubHandler > should create correct GitHubContext spot for PR comment > github-pr-517-spot 1`] = ` { - "commentId": undefined, "domain": "github.com", "number": 517, "slug": "diffplug/selfie", diff --git a/browser-extension/tests/datamodel/handlers/github-handler.test.ts b/browser-extension/tests/datamodel/handlers/github-handler.test.ts index 0946a87..75392a0 100644 --- a/browser-extension/tests/datamodel/handlers/github-handler.test.ts +++ b/browser-extension/tests/datamodel/handlers/github-handler.test.ts @@ -67,7 +67,6 @@ describe('GitHubHandler', () => { // Also verify specific expected values expect(enhancedTextarea?.spot).toMatchObject({ - commentId: undefined, domain: 'github.com', number: 517, slug: 'diffplug/selfie', @@ -76,32 +75,6 @@ describe('GitHubHandler', () => { }) }) - it('should handle multiple textareas on the same page', () => { - // Create a second textarea for inline code comments - const codeCommentTextarea = document.createElement('textarea') - codeCommentTextarea.className = 'form-control js-suggester-field' - - const inlineForm = document.createElement('div') - inlineForm.className = 'js-inline-comment-form' - inlineForm.appendChild(codeCommentTextarea) - document.body.appendChild(inlineForm) - - // Test both textareas - const mainCommentEnhanced = enhancers.tryToEnhance(mockTextarea) - const codeCommentEnhanced = enhancers.tryToEnhance(codeCommentTextarea) - - expect(mainCommentEnhanced?.spot.type).toBe('GH_PR_ADD_COMMENT') - expect(codeCommentEnhanced?.spot.type).toBe('GH_PR_CODE_COMMENT') - - // Register both - if (mainCommentEnhanced) enhancedTextareas.register(mainCommentEnhanced) - if (codeCommentEnhanced) enhancedTextareas.register(codeCommentEnhanced) - - // Verify both are registered - expect(enhancedTextareas.get(mockTextarea)).toBeTruthy() - expect(enhancedTextareas.get(codeCommentTextarea)).toBeTruthy() - }) - it('should not enhance textarea on non-GitHub pages', () => { // Change location to non-GitHub site Object.defineProperty(window, 'location', { From 3f15b240dd887caa3a3dee01d31738fa178fa631 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 11:04:33 -0700 Subject: [PATCH 39/54] highlight.js not highlightjs --- browser-extension/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-extension/README.md b/browser-extension/README.md index 8eed095..24c46df 100644 --- a/browser-extension/README.md +++ b/browser-extension/README.md @@ -27,7 +27,7 @@ This is a [WXT](https://wxt.dev/)-based browser extension that -- finds `textarea` components and decorates them with [overtype](https://overtype.dev/) and [highlightjs](https://highlightjs.org/) +- finds `textarea` components and decorates them with [overtype](https://overtype.dev/) and [highlight.js](https://highlightjs.org/) - stores unposted comment drafts, and makes them easy to find via the extension popup ### Entry points From e7cb0de83f4b9ec597e8dd89c93f38c8eabc5733 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 11:08:41 -0700 Subject: [PATCH 40/54] Cleanup via renames. --- .../src/datamodel/handlers/github-handler.ts | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/browser-extension/src/datamodel/handlers/github-handler.ts b/browser-extension/src/datamodel/handlers/github-handler.ts index 2b59f4c..24e09e5 100644 --- a/browser-extension/src/datamodel/handlers/github-handler.ts +++ b/browser-extension/src/datamodel/handlers/github-handler.ts @@ -15,19 +15,19 @@ const GITHUB_COMMENT_TYPES = [ export type GitHubCommentType = (typeof GITHUB_COMMENT_TYPES)[number] -export interface GitHubContext extends CommentSpot { +export interface GitHubSpot extends CommentSpot { type: GitHubCommentType // Override to narrow from string to specific union domain: string slug: string // owner/repo number?: number | undefined // issue/PR number, undefined for new issues and PRs } -export class GitHubHandler implements CommentEnhancer { +export class GitHubHandler implements CommentEnhancer { forCommentTypes(): string[] { return [...GITHUB_COMMENT_TYPES] } - tryToEnhance(textarea: HTMLTextAreaElement): [OverType, GitHubContext] | null { + tryToEnhance(textarea: HTMLTextAreaElement): [OverType, GitHubSpot] | null { // Only handle GitHub domains if (!window.location.hostname.includes('github')) { return null @@ -46,16 +46,11 @@ export class GitHubHandler implements CommentEnhancer { // Determine comment type let type: GitHubCommentType - // New issue if (pathname.includes('/issues/new')) { type = 'GH_ISSUE_NEW' - } - // New PR - else if (pathname.includes('/compare/') || pathname.endsWith('/compare')) { + } else if (pathname.includes('/compare/') || pathname.endsWith('/compare')) { type = 'GH_PR_NEW' - } - // Existing issue or PR page - else if (urlType && number) { + } else if (urlType && number) { if (urlType === 'issues') { type = 'GH_ISSUE_ADD_COMMENT' } else { @@ -73,30 +68,27 @@ export class GitHubHandler implements CommentEnhancer { unique_key += ':new' } - const context: GitHubContext = { + const spot: GitHubSpot = { domain: window.location.hostname, number, slug, type, unique_key, } - - // Create OverType instance for this textarea const overtype = new OverType(textarea) - - return [overtype, context] + return [overtype, spot] } - generateDisplayTitle(context: GitHubContext): string { - const { slug, number } = context + generateDisplayTitle(spot: GitHubSpot): string { + const { slug, number } = spot if (number) { return `Comment on ${slug} #${number}` } return `New ${window.location.pathname.includes('/issues/') ? 'issue' : 'PR'} in ${slug}` } - generateIcon(context: GitHubContext): string { - switch (context.type) { + generateIcon(spot: GitHubSpot): string { + switch (spot.type) { case 'GH_ISSUE_NEW': case 'GH_ISSUE_ADD_COMMENT': return '🐛' // Issue icon @@ -106,14 +98,12 @@ export class GitHubHandler implements CommentEnhancer { } } - buildUrl(context: GitHubContext): string { - const baseUrl = `https://${context.domain}/${context.slug}` - - if (context.number) { - const type = window.location.pathname.includes('/issues/') ? 'issues' : 'pull' - return `${baseUrl}/${type}/${context.number}` + buildUrl(spot: GitHubSpot): string { + const baseUrl = `https://${spot.domain}/${spot.slug}` + if (spot.number) { + const type = spot.type.indexOf('ISSUE') ? 'issues' : 'pull' + return `${baseUrl}/${type}/${spot.number}` } - return baseUrl } } From 7ad747dbf195da406e7be6a8b11a6cf14613ea2a Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 11:12:36 -0700 Subject: [PATCH 41/54] More renames. --- browser-extension/src/datamodel/enhancer.ts | 10 +++++----- .../github-handler.ts => enhancers/github.ts} | 2 +- browser-extension/src/datamodel/registries.ts | 2 +- .../__snapshots__/github.test.ts.snap} | 0 .../github.test.ts} | 0 5 files changed, 7 insertions(+), 7 deletions(-) rename browser-extension/src/datamodel/{handlers/github-handler.ts => enhancers/github.ts} (97%) rename browser-extension/tests/datamodel/{handlers/__snapshots__/github-handler.test.ts.snap => enhancers/__snapshots__/github.test.ts.snap} (100%) rename browser-extension/tests/datamodel/{handlers/github-handler.test.ts => enhancers/github.test.ts} (100%) diff --git a/browser-extension/src/datamodel/enhancer.ts b/browser-extension/src/datamodel/enhancer.ts index d5c4c31..03faf8e 100644 --- a/browser-extension/src/datamodel/enhancer.ts +++ b/browser-extension/src/datamodel/enhancer.ts @@ -11,16 +11,16 @@ export interface CommentSpot { } /** wraps the textareas of a given platform with Gitcasso's enhancements */ -export interface CommentEnhancer { +export interface CommentEnhancer { /** guarantees to only return a type within this list */ forCommentTypes(): string[] /** * whenever a new `textarea` is added to any webpage, this method is called. * if we return non-null, then we become the handler for that text area. */ - tryToEnhance(textarea: HTMLTextAreaElement): [OverType, T] | null + tryToEnhance(textarea: HTMLTextAreaElement): [OverType, Spot] | null - generateIcon(context: T): string - generateDisplayTitle(context: T): string - buildUrl(context: T, withDraft?: boolean): string + generateIcon(spot: Spot): string + generateDisplayTitle(spot: Spot): string + buildUrl(spot: Spot): string } diff --git a/browser-extension/src/datamodel/handlers/github-handler.ts b/browser-extension/src/datamodel/enhancers/github.ts similarity index 97% rename from browser-extension/src/datamodel/handlers/github-handler.ts rename to browser-extension/src/datamodel/enhancers/github.ts index 24e09e5..0666d33 100644 --- a/browser-extension/src/datamodel/handlers/github-handler.ts +++ b/browser-extension/src/datamodel/enhancers/github.ts @@ -22,7 +22,7 @@ export interface GitHubSpot extends CommentSpot { number?: number | undefined // issue/PR number, undefined for new issues and PRs } -export class GitHubHandler implements CommentEnhancer { +export class GitHubEnhancer implements CommentEnhancer { forCommentTypes(): string[] { return [...GITHUB_COMMENT_TYPES] } diff --git a/browser-extension/src/datamodel/registries.ts b/browser-extension/src/datamodel/registries.ts index 27e5f3d..4299e46 100644 --- a/browser-extension/src/datamodel/registries.ts +++ b/browser-extension/src/datamodel/registries.ts @@ -1,6 +1,6 @@ import type { OverType } from '../overtype/mock-overtype' import type { CommentEnhancer, CommentSpot } from './enhancer' -import { GitHubHandler as GitHubEnhancer } from './handlers/github-handler' +import { GitHubEnhancer } from './enhancers/github' export interface EnhancedTextarea { element: HTMLTextAreaElement diff --git a/browser-extension/tests/datamodel/handlers/__snapshots__/github-handler.test.ts.snap b/browser-extension/tests/datamodel/enhancers/__snapshots__/github.test.ts.snap similarity index 100% rename from browser-extension/tests/datamodel/handlers/__snapshots__/github-handler.test.ts.snap rename to browser-extension/tests/datamodel/enhancers/__snapshots__/github.test.ts.snap diff --git a/browser-extension/tests/datamodel/handlers/github-handler.test.ts b/browser-extension/tests/datamodel/enhancers/github.test.ts similarity index 100% rename from browser-extension/tests/datamodel/handlers/github-handler.test.ts rename to browser-extension/tests/datamodel/enhancers/github.test.ts From b9c730ef20e7ad6a0c94fb8fc09676a75bd1171b Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 11:13:31 -0700 Subject: [PATCH 42/54] Minor cleanup. --- browser-extension/src/datamodel/registries.ts | 6 +++--- browser-extension/tests/datamodel/enhancers/github.test.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/browser-extension/src/datamodel/registries.ts b/browser-extension/src/datamodel/registries.ts index 4299e46..51ccf00 100644 --- a/browser-extension/src/datamodel/registries.ts +++ b/browser-extension/src/datamodel/registries.ts @@ -3,7 +3,7 @@ import type { CommentEnhancer, CommentSpot } from './enhancer' import { GitHubEnhancer } from './enhancers/github' export interface EnhancedTextarea { - element: HTMLTextAreaElement + textarea: HTMLTextAreaElement spot: T handler: CommentEnhancer overtype: OverType @@ -36,7 +36,7 @@ export class EnhancerRegistry { const result = handler.tryToEnhance(textarea) if (result) { const [overtype, spot] = result - return { element: textarea, handler, overtype, spot } + return { handler, overtype, spot, textarea } } } catch (error) { console.warn('Handler failed to identify textarea:', error) @@ -58,7 +58,7 @@ export class TextareaRegistry { private textareas = new Map>() register(textareaInfo: EnhancedTextarea): void { - this.textareas.set(textareaInfo.element, textareaInfo) + this.textareas.set(textareaInfo.textarea, textareaInfo) // TODO: register as a draft in progress with the global list } diff --git a/browser-extension/tests/datamodel/enhancers/github.test.ts b/browser-extension/tests/datamodel/enhancers/github.test.ts index 75392a0..da75bc4 100644 --- a/browser-extension/tests/datamodel/enhancers/github.test.ts +++ b/browser-extension/tests/datamodel/enhancers/github.test.ts @@ -43,7 +43,7 @@ describe('GitHubHandler', () => { const enhancedTextarea = enhancers.tryToEnhance(mockTextarea) expect(enhancedTextarea).toBeTruthy() - expect(enhancedTextarea?.element).toBe(mockTextarea) + expect(enhancedTextarea?.textarea).toBe(mockTextarea) expect(enhancedTextarea?.spot.type).toBe('GH_PR_ADD_COMMENT') // Register the enhanced textarea @@ -54,7 +54,7 @@ describe('GitHubHandler', () => { // Verify it's in the registry const registeredTextarea = enhancedTextareas.get(mockTextarea) expect(registeredTextarea).toBeTruthy() - expect(registeredTextarea?.element).toBe(mockTextarea) + expect(registeredTextarea?.textarea).toBe(mockTextarea) }) it('should create correct GitHubContext spot for PR comment', () => { From 8b6f80be41c02961e0b138ca18f9db0fd7193cd7 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 11:15:53 -0700 Subject: [PATCH 43/54] More cleanup. --- browser-extension/src/datamodel/enhancer.ts | 2 +- .../src/datamodel/enhancers/github.ts | 2 +- browser-extension/src/datamodel/registries.ts | 17 ++--------------- browser-extension/src/entrypoints/content.ts | 2 +- 4 files changed, 5 insertions(+), 18 deletions(-) diff --git a/browser-extension/src/datamodel/enhancer.ts b/browser-extension/src/datamodel/enhancer.ts index 03faf8e..5ef4e4c 100644 --- a/browser-extension/src/datamodel/enhancer.ts +++ b/browser-extension/src/datamodel/enhancer.ts @@ -13,7 +13,7 @@ export interface CommentSpot { /** wraps the textareas of a given platform with Gitcasso's enhancements */ export interface CommentEnhancer { /** guarantees to only return a type within this list */ - forCommentTypes(): string[] + forSpotTypes(): string[] /** * whenever a new `textarea` is added to any webpage, this method is called. * if we return non-null, then we become the handler for that text area. diff --git a/browser-extension/src/datamodel/enhancers/github.ts b/browser-extension/src/datamodel/enhancers/github.ts index 0666d33..dca6e8b 100644 --- a/browser-extension/src/datamodel/enhancers/github.ts +++ b/browser-extension/src/datamodel/enhancers/github.ts @@ -23,7 +23,7 @@ export interface GitHubSpot extends CommentSpot { } export class GitHubEnhancer implements CommentEnhancer { - forCommentTypes(): string[] { + forSpotTypes(): string[] { return [...GITHUB_COMMENT_TYPES] } diff --git a/browser-extension/src/datamodel/registries.ts b/browser-extension/src/datamodel/registries.ts index 51ccf00..467043a 100644 --- a/browser-extension/src/datamodel/registries.ts +++ b/browser-extension/src/datamodel/registries.ts @@ -21,15 +21,6 @@ export class EnhancerRegistry { this.enhancers.add(handler) } - getHandlerForType(type: string): CommentEnhancer | null { - for (const handler of this.enhancers) { - if (handler.forCommentTypes().includes(type)) { - return handler - } - } - return null - } - tryToEnhance(textarea: HTMLTextAreaElement): EnhancedTextarea | null { for (const handler of this.enhancers) { try { @@ -45,12 +36,8 @@ export class EnhancerRegistry { return null } - getAllHandlers(): CommentEnhancer[] { - return Array.from(this.enhancers) - } - - getCommentTypesForHandler(handler: CommentEnhancer): string[] { - return handler.forCommentTypes() + getEnhancerCount(): number { + return this.enhancers.size } } diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 14feab2..10b9e08 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -16,7 +16,7 @@ export default defineContentScript({ childList: true, subtree: true, }) - logger.debug('Extension loaded with', enhancers.getAllHandlers().length, 'handlers') + logger.debug('Extension loaded with', enhancers.getEnhancerCount, 'handlers') }, matches: [''], runAt: 'document_end', From dbdb7564d4254c6b20bf58d405233c0c1fa7a6e9 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 11:17:16 -0700 Subject: [PATCH 44/54] More rename. --- browser-extension/src/datamodel/enhancer.ts | 4 ++-- browser-extension/src/datamodel/enhancers/github.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/browser-extension/src/datamodel/enhancer.ts b/browser-extension/src/datamodel/enhancer.ts index 5ef4e4c..82ac2eb 100644 --- a/browser-extension/src/datamodel/enhancer.ts +++ b/browser-extension/src/datamodel/enhancer.ts @@ -20,7 +20,7 @@ export interface CommentEnhancer { */ tryToEnhance(textarea: HTMLTextAreaElement): [OverType, Spot] | null - generateIcon(spot: Spot): string - generateDisplayTitle(spot: Spot): string + tableIcon(spot: Spot): string + tableTitle(spot: Spot): string buildUrl(spot: Spot): string } diff --git a/browser-extension/src/datamodel/enhancers/github.ts b/browser-extension/src/datamodel/enhancers/github.ts index dca6e8b..b611b28 100644 --- a/browser-extension/src/datamodel/enhancers/github.ts +++ b/browser-extension/src/datamodel/enhancers/github.ts @@ -79,7 +79,7 @@ export class GitHubEnhancer implements CommentEnhancer { return [overtype, spot] } - generateDisplayTitle(spot: GitHubSpot): string { + tableTitle(spot: GitHubSpot): string { const { slug, number } = spot if (number) { return `Comment on ${slug} #${number}` @@ -87,7 +87,7 @@ export class GitHubEnhancer implements CommentEnhancer { return `New ${window.location.pathname.includes('/issues/') ? 'issue' : 'PR'} in ${slug}` } - generateIcon(spot: GitHubSpot): string { + tableIcon(spot: GitHubSpot): string { switch (spot.type) { case 'GH_ISSUE_NEW': case 'GH_ISSUE_ADD_COMMENT': From 9466025cbd1d5bf2198441bdd514cf2bbdb8a0da Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 11:18:41 -0700 Subject: [PATCH 45/54] Minor rename. --- browser-extension/src/datamodel/enhancers/github.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/browser-extension/src/datamodel/enhancers/github.ts b/browser-extension/src/datamodel/enhancers/github.ts index b611b28..a055263 100644 --- a/browser-extension/src/datamodel/enhancers/github.ts +++ b/browser-extension/src/datamodel/enhancers/github.ts @@ -1,7 +1,7 @@ import { OverType } from '../../overtype/mock-overtype' import type { CommentEnhancer, CommentSpot } from '../enhancer' -const GITHUB_COMMENT_TYPES = [ +const GITHUB_SPOT_TYPES = [ 'GH_ISSUE_NEW', 'GH_PR_NEW', 'GH_ISSUE_ADD_COMMENT', @@ -13,10 +13,10 @@ const GITHUB_COMMENT_TYPES = [ */ ] as const -export type GitHubCommentType = (typeof GITHUB_COMMENT_TYPES)[number] +export type GitHubSpotType = (typeof GITHUB_SPOT_TYPES)[number] export interface GitHubSpot extends CommentSpot { - type: GitHubCommentType // Override to narrow from string to specific union + type: GitHubSpotType // Override to narrow from string to specific union domain: string slug: string // owner/repo number?: number | undefined // issue/PR number, undefined for new issues and PRs @@ -24,7 +24,7 @@ export interface GitHubSpot extends CommentSpot { export class GitHubEnhancer implements CommentEnhancer { forSpotTypes(): string[] { - return [...GITHUB_COMMENT_TYPES] + return [...GITHUB_SPOT_TYPES] } tryToEnhance(textarea: HTMLTextAreaElement): [OverType, GitHubSpot] | null { @@ -44,7 +44,7 @@ export class GitHubEnhancer implements CommentEnhancer { const number = numberStr ? parseInt(numberStr, 10) : undefined // Determine comment type - let type: GitHubCommentType + let type: GitHubSpotType if (pathname.includes('/issues/new')) { type = 'GH_ISSUE_NEW' From 22c826ac56fc00ceacd9370472e7869ffb6e4eed Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 11:23:07 -0700 Subject: [PATCH 46/54] Remove unnecessary comments. --- browser-extension/src/entrypoints/content.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index 10b9e08..cc5a04b 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -24,7 +24,6 @@ export default defineContentScript({ function handleMutations(mutations: MutationRecord[]): void { for (const mutation of mutations) { - // Handle added nodes for (const node of mutation.addedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as Element @@ -42,7 +41,6 @@ function handleMutations(mutations: MutationRecord[]): void { } } - // Handle removed nodes for (const node of mutation.removedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as Element @@ -63,7 +61,6 @@ function handleMutations(mutations: MutationRecord[]): void { } function enhanceMaybe(textarea: HTMLTextAreaElement) { - // Check if this textarea is already registered if (enhancedTextareas.get(textarea)) { logger.debug('textarea already registered {}', textarea) return @@ -72,7 +69,6 @@ function enhanceMaybe(textarea: HTMLTextAreaElement) { logger.debug('activating textarea {}', textarea) injectStyles() - // Use registry to identify and handle this specific textarea const enhancedTextarea = enhancers.tryToEnhance(textarea) if (enhancedTextarea) { logger.debug( From 48fb4fa8956985c6e0dea1cb89891e0cdbab8083 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 11:32:25 -0700 Subject: [PATCH 47/54] datamodel -> logic, common -> util --- browser-extension/src/entrypoints/content.ts | 6 +++--- browser-extension/src/{datamodel => logic}/enhancer.ts | 0 .../src/{datamodel => logic}/enhancers/github.ts | 0 browser-extension/src/{datamodel => logic}/registries.ts | 0 browser-extension/src/{common => util}/config.ts | 0 browser-extension/src/{common => util}/logger.ts | 0 browser-extension/tests/datamodel/enhancers/github.test.ts | 2 +- 7 files changed, 4 insertions(+), 4 deletions(-) rename browser-extension/src/{datamodel => logic}/enhancer.ts (100%) rename browser-extension/src/{datamodel => logic}/enhancers/github.ts (100%) rename browser-extension/src/{datamodel => logic}/registries.ts (100%) rename browser-extension/src/{common => util}/config.ts (100%) rename browser-extension/src/{common => util}/logger.ts (100%) diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index cc5a04b..ffd7673 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -1,6 +1,6 @@ -import { CONFIG } from '../common/config' -import { logger } from '../common/logger' -import { EnhancerRegistry, TextareaRegistry } from '../datamodel/registries' +import { EnhancerRegistry, TextareaRegistry } from '../logic/registries' +import { CONFIG } from '../util/config' +import { logger } from '../util/logger' const enhancers = new EnhancerRegistry() const enhancedTextareas = new TextareaRegistry() diff --git a/browser-extension/src/datamodel/enhancer.ts b/browser-extension/src/logic/enhancer.ts similarity index 100% rename from browser-extension/src/datamodel/enhancer.ts rename to browser-extension/src/logic/enhancer.ts diff --git a/browser-extension/src/datamodel/enhancers/github.ts b/browser-extension/src/logic/enhancers/github.ts similarity index 100% rename from browser-extension/src/datamodel/enhancers/github.ts rename to browser-extension/src/logic/enhancers/github.ts diff --git a/browser-extension/src/datamodel/registries.ts b/browser-extension/src/logic/registries.ts similarity index 100% rename from browser-extension/src/datamodel/registries.ts rename to browser-extension/src/logic/registries.ts diff --git a/browser-extension/src/common/config.ts b/browser-extension/src/util/config.ts similarity index 100% rename from browser-extension/src/common/config.ts rename to browser-extension/src/util/config.ts diff --git a/browser-extension/src/common/logger.ts b/browser-extension/src/util/logger.ts similarity index 100% rename from browser-extension/src/common/logger.ts rename to browser-extension/src/util/logger.ts diff --git a/browser-extension/tests/datamodel/enhancers/github.test.ts b/browser-extension/tests/datamodel/enhancers/github.test.ts index da75bc4..08e9f84 100644 --- a/browser-extension/tests/datamodel/enhancers/github.test.ts +++ b/browser-extension/tests/datamodel/enhancers/github.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { EnhancerRegistry, TextareaRegistry } from '../../../src/datamodel/registries' +import { EnhancerRegistry, TextareaRegistry } from '../../../src/logic/registries' // Mock WXT's defineContentScript global vi.stubGlobal('defineContentScript', vi.fn()) From 2feab1863f0ab4b5affc275ee79d1ed38ee039d3 Mon Sep 17 00:00:00 2001 From: ntwigg Date: Thu, 4 Sep 2025 11:38:39 -0700 Subject: [PATCH 48/54] logic -> lib --- browser-extension/src/entrypoints/content.ts | 6 +++--- browser-extension/src/{util => lib}/config.ts | 0 browser-extension/src/{logic => lib}/enhancer.ts | 0 browser-extension/src/{logic => lib}/enhancers/github.ts | 0 browser-extension/src/{util => lib}/logger.ts | 0 browser-extension/src/{logic => lib}/registries.ts | 0 .../enhancers/__snapshots__/github.test.ts.snap | 0 .../tests/{datamodel => lib}/enhancers/github.test.ts | 2 +- 8 files changed, 4 insertions(+), 4 deletions(-) rename browser-extension/src/{util => lib}/config.ts (100%) rename browser-extension/src/{logic => lib}/enhancer.ts (100%) rename browser-extension/src/{logic => lib}/enhancers/github.ts (100%) rename browser-extension/src/{util => lib}/logger.ts (100%) rename browser-extension/src/{logic => lib}/registries.ts (100%) rename browser-extension/tests/{datamodel => lib}/enhancers/__snapshots__/github.test.ts.snap (100%) rename browser-extension/tests/{datamodel => lib}/enhancers/github.test.ts (99%) diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index ffd7673..e8041dc 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -1,6 +1,6 @@ -import { EnhancerRegistry, TextareaRegistry } from '../logic/registries' -import { CONFIG } from '../util/config' -import { logger } from '../util/logger' +import { CONFIG } from '../lib/config' +import { logger } from '../lib/logger' +import { EnhancerRegistry, TextareaRegistry } from '../lib/registries' const enhancers = new EnhancerRegistry() const enhancedTextareas = new TextareaRegistry() diff --git a/browser-extension/src/util/config.ts b/browser-extension/src/lib/config.ts similarity index 100% rename from browser-extension/src/util/config.ts rename to browser-extension/src/lib/config.ts diff --git a/browser-extension/src/logic/enhancer.ts b/browser-extension/src/lib/enhancer.ts similarity index 100% rename from browser-extension/src/logic/enhancer.ts rename to browser-extension/src/lib/enhancer.ts diff --git a/browser-extension/src/logic/enhancers/github.ts b/browser-extension/src/lib/enhancers/github.ts similarity index 100% rename from browser-extension/src/logic/enhancers/github.ts rename to browser-extension/src/lib/enhancers/github.ts diff --git a/browser-extension/src/util/logger.ts b/browser-extension/src/lib/logger.ts similarity index 100% rename from browser-extension/src/util/logger.ts rename to browser-extension/src/lib/logger.ts diff --git a/browser-extension/src/logic/registries.ts b/browser-extension/src/lib/registries.ts similarity index 100% rename from browser-extension/src/logic/registries.ts rename to browser-extension/src/lib/registries.ts diff --git a/browser-extension/tests/datamodel/enhancers/__snapshots__/github.test.ts.snap b/browser-extension/tests/lib/enhancers/__snapshots__/github.test.ts.snap similarity index 100% rename from browser-extension/tests/datamodel/enhancers/__snapshots__/github.test.ts.snap rename to browser-extension/tests/lib/enhancers/__snapshots__/github.test.ts.snap diff --git a/browser-extension/tests/datamodel/enhancers/github.test.ts b/browser-extension/tests/lib/enhancers/github.test.ts similarity index 99% rename from browser-extension/tests/datamodel/enhancers/github.test.ts rename to browser-extension/tests/lib/enhancers/github.test.ts index 08e9f84..2ec59ef 100644 --- a/browser-extension/tests/datamodel/enhancers/github.test.ts +++ b/browser-extension/tests/lib/enhancers/github.test.ts @@ -1,5 +1,5 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' -import { EnhancerRegistry, TextareaRegistry } from '../../../src/logic/registries' +import { EnhancerRegistry, TextareaRegistry } from '../../../src/lib/registries' // Mock WXT's defineContentScript global vi.stubGlobal('defineContentScript', vi.fn()) From f92f6cb91b38bb9aa7eeadfe57af74fe5d79d32b Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 4 Sep 2025 11:57:15 -0700 Subject: [PATCH 49/54] Exclude `src/overtype` from biome --- browser-extension/biome.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-extension/biome.json b/browser-extension/biome.json index d4cccec..9d4e34e 100644 --- a/browser-extension/biome.json +++ b/browser-extension/biome.json @@ -10,7 +10,7 @@ }, "files": { "ignoreUnknown": false, - "includes": [".*", "src/**", "tests/**"] + "includes": [".*", "src/**", "tests/**", "!src/overtype"] }, "formatter": { "enabled": true, From 8fa7f19c650a89fcfd38b5def62f02b031436618 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 4 Sep 2025 12:11:40 -0700 Subject: [PATCH 50/54] Add the POC script as `github-playground` --- .../src/playgrounds/github-playground.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 browser-extension/src/playgrounds/github-playground.ts diff --git a/browser-extension/src/playgrounds/github-playground.ts b/browser-extension/src/playgrounds/github-playground.ts new file mode 100644 index 0000000..44e4207 --- /dev/null +++ b/browser-extension/src/playgrounds/github-playground.ts @@ -0,0 +1,33 @@ +import OverType from "../overtype/overtype"; + +export function githubPrNewCommentContentScript() { + if (window.location.hostname !== "github.com") { + return; + } + + const ghCommentBox = document.getElementById( + "new_comment_field" + ) as HTMLTextAreaElement | null; + if (ghCommentBox) { + const overtypeContainer = modifyDOM(ghCommentBox); + new OverType(overtypeContainer, { + placeholder: "Add your comment here...", + autoResize: true, + minHeight: "102px", + padding: "var(--base-size-8)", + }); + } + } + +function modifyDOM(overtypeInput: HTMLTextAreaElement): HTMLElement { + overtypeInput.classList.add("overtype-input"); + const overtypePreview = document.createElement("div"); + overtypePreview.classList.add("overtype-preview"); + overtypeInput.insertAdjacentElement("afterend", overtypePreview); + const overtypeWrapper = overtypeInput.parentElement!.closest("div")!; + overtypeWrapper.classList.add("overtype-wrapper"); + overtypeInput.placeholder = "Add your comment here..."; + const overtypeContainer = overtypeWrapper.parentElement!.closest("div")!; + overtypeContainer.classList.add("overtype-container"); + return overtypeContainer.parentElement!.closest("div")!; +} From e7351866555fab882f75c52c734bda0fdec47e9e Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 4 Sep 2025 12:12:06 -0700 Subject: [PATCH 51/54] fixup package-lock.json --- browser-extension/package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/browser-extension/package-lock.json b/browser-extension/package-lock.json index 4835387..368b7aa 100644 --- a/browser-extension/package-lock.json +++ b/browser-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "gitcasso", - "version": "1.0.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gitcasso", - "version": "1.0.0", + "version": "0.0.1", "hasInstallScript": true, "license": "MIT", "dependencies": { From 854d10c22773b5ff10aba5c709b6b457e7386f59 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 4 Sep 2025 12:12:24 -0700 Subject: [PATCH 52/54] Exclude the playgrounds from biome --- browser-extension/biome.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser-extension/biome.json b/browser-extension/biome.json index 9d4e34e..5dd9019 100644 --- a/browser-extension/biome.json +++ b/browser-extension/biome.json @@ -10,7 +10,7 @@ }, "files": { "ignoreUnknown": false, - "includes": [".*", "src/**", "tests/**", "!src/overtype"] + "includes": [".*", "src/**", "tests/**", "!src/overtype", "!src/playgrounds"] }, "formatter": { "enabled": true, From 418494162ede582d58a2616a0e7b3f10a312e032 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 4 Sep 2025 12:12:47 -0700 Subject: [PATCH 53/54] Wire up the playground. --- browser-extension/src/entrypoints/content.ts | 5 +++++ browser-extension/src/lib/config.ts | 13 +++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/browser-extension/src/entrypoints/content.ts b/browser-extension/src/entrypoints/content.ts index e8041dc..1610d92 100644 --- a/browser-extension/src/entrypoints/content.ts +++ b/browser-extension/src/entrypoints/content.ts @@ -1,12 +1,17 @@ import { CONFIG } from '../lib/config' 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() export default defineContentScript({ main() { + if (CONFIG.MODE === 'PLAYGROUNDS_PR') { + githubPrNewCommentContentScript() + return + } const textAreasOnPageLoad = document.querySelectorAll(`textarea`) for (const textarea of textAreasOnPageLoad) { enhanceMaybe(textarea) diff --git a/browser-extension/src/lib/config.ts b/browser-extension/src/lib/config.ts index c4f0a86..1f5e748 100644 --- a/browser-extension/src/lib/config.ts +++ b/browser-extension/src/lib/config.ts @@ -1,9 +1,10 @@ -// Configuration constants for the extension +const MODES = ['PROD', 'PLAYGROUNDS_PR'] as const + +export type ModeType = (typeof MODES)[number] + export const CONFIG = { ADDED_OVERTYPE_CLASS: 'gitcasso-overtype', - // Debug settings - DEBUG: true, // Set to true to enable debug logging - EXTENSION_NAME: 'gitcasso', - INITIAL_SCAN_DELAY_MS: 100, - MUTATION_OBSERVER_DELAY_MS: 100, + DEBUG: true, // enabled debug logging + EXTENSION_NAME: 'gitcasso', // decorates logs + MODE: 'PLAYGROUNDS_PR' satisfies ModeType, } as const From a68d74216921ef2dc7c9da3a4a82c6374fddbc03 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 4 Sep 2025 13:04:27 -0700 Subject: [PATCH 54/54] Pull latest overtype-hard changes into the github-playground. --- .../src/playgrounds/github-playground.ts | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/browser-extension/src/playgrounds/github-playground.ts b/browser-extension/src/playgrounds/github-playground.ts index 44e4207..dae988f 100644 --- a/browser-extension/src/playgrounds/github-playground.ts +++ b/browser-extension/src/playgrounds/github-playground.ts @@ -1,10 +1,11 @@ +import hljs from "highlight.js"; import OverType from "../overtype/overtype"; export function githubPrNewCommentContentScript() { if (window.location.hostname !== "github.com") { return; } - + OverType.setCodeHighlighter(hljsHighlighter); const ghCommentBox = document.getElementById( "new_comment_field" ) as HTMLTextAreaElement | null; @@ -17,7 +18,7 @@ export function githubPrNewCommentContentScript() { padding: "var(--base-size-8)", }); } - } +} function modifyDOM(overtypeInput: HTMLTextAreaElement): HTMLElement { overtypeInput.classList.add("overtype-input"); @@ -31,3 +32,18 @@ function modifyDOM(overtypeInput: HTMLTextAreaElement): HTMLElement { overtypeContainer.classList.add("overtype-container"); return overtypeContainer.parentElement!.closest("div")!; } + +function hljsHighlighter(code: string, language: string) { + try { + if (language && hljs.getLanguage(language)) { + const result = hljs.highlight(code, { language }); + return result.value; + } else { + const result = hljs.highlightAuto(code); + return result.value; + } + } catch (error) { + console.warn("highlight.js highlighting failed:", error); + return code; + } +}