From d33b16778e6edcbec3ab47fee27f57a3c94cc3d9 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 29 Sep 2025 09:58:08 -0700 Subject: [PATCH 1/4] Add `prettier` config. --- prettier.config.cjs | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 prettier.config.cjs diff --git a/prettier.config.cjs b/prettier.config.cjs new file mode 100644 index 0000000..7ab985a --- /dev/null +++ b/prettier.config.cjs @@ -0,0 +1,8 @@ +/** @type {import('prettier').Config} */ +module.exports = { + endOfLine: "lf", + semi: false, + singleQuote: false, + tabWidth: 2, + trailingComma: "es5", +} From f7940d84dbf3641a62c979b0cdca91563240097c Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 29 Sep 2025 09:58:18 -0700 Subject: [PATCH 2/4] Set biome to match. --- biome.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/biome.json b/biome.json index 1056f6b..9f1d661 100644 --- a/biome.json +++ b/biome.json @@ -18,19 +18,22 @@ "indentStyle": "space", "indentWidth": 2, "lineEnding": "lf", - "lineWidth": 100 + "lineWidth": 80 }, "javascript": { "formatter": { "arrowParentheses": "always", - "attributePosition": "auto", "bracketSameLine": false, "bracketSpacing": true, - "jsxQuoteStyle": "single", + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "jsxQuoteStyle": "double", + "lineEnding": "lf", "quoteProperties": "asNeeded", - "quoteStyle": "single", + "quoteStyle": "double", "semicolons": "asNeeded", - "trailingCommas": "all" + "trailingCommas": "es5" } }, "json": { From f86f006ce22e044c39caa0942cb9609d83863df6 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 29 Sep 2025 09:59:08 -0700 Subject: [PATCH 3/4] format all files to match (mostly ' => ") --- playwright.config.ts | 12 +- src/components/Badge.tsx | 42 +-- src/components/BadgePopups/CodePreview.tsx | 9 +- src/components/BadgePopups/ImagePreview.tsx | 9 +- src/components/BadgePopups/LinkPreview.tsx | 9 +- src/components/BadgePopups/OpenTabPopup.tsx | 8 +- src/components/BadgePopups/TextPreview.tsx | 9 +- src/components/BadgePopups/TimePreview.tsx | 9 +- src/components/BulkActionsBar.tsx | 12 +- src/components/CommentRow.tsx | 82 ++++-- src/components/EmptyState.tsx | 18 +- src/components/MultiSegment.tsx | 22 +- src/components/NoMatchesState.tsx | 10 +- src/components/PopupRoot.tsx | 161 ++++++----- src/components/design.tsx | 38 +-- src/components/misc.ts | 29 +- src/entrypoints/background.ts | 105 ++++--- src/entrypoints/content.ts | 55 ++-- src/entrypoints/popup/popup.tsx | 38 +-- src/entrypoints/popup/style.css | 3 +- src/lib/config.ts | 10 +- src/lib/enhancer.ts | 11 +- src/lib/enhancers/CommentEnhancerMissing.tsx | 48 ++-- .../enhancers/github/GitHubEditEnhancer.tsx | 52 ++-- .../github/GitHubIssueAppendEnhancer.tsx | 56 ++-- .../github/GitHubIssueCreateEnhancer.tsx | 52 ++-- .../github/GitHubPrAppendEnhancer.tsx | 51 ++-- .../github/GitHubPrCreateEnhancer.tsx | 61 ++-- src/lib/enhancers/github/github-common.ts | 16 +- src/lib/enhancers/modifyDOM.ts | 51 ++-- src/lib/logger.ts | 14 +- src/lib/messages.ts | 44 +-- src/lib/registries.ts | 67 +++-- tests/background.test.ts | 62 ++-- tests/corpus-fixture.ts | 30 +- tests/corpus-har-record.ts | 72 ++--- tests/corpus-utils.ts | 44 +-- tests/corpus-view.ts | 228 ++++++++------- tests/corpus/_corpus-index.ts | 75 ++--- tests/lib/enhancers/draft-stats.test.ts | 40 +-- tests/lib/enhancers/gh-detection.test.ts | 19 +- tests/lib/enhancers/gh-ui.test.ts | 14 +- tests/playground/claude.tsx | 265 +++++++++++------- tests/playground/playground.tsx | 47 ++-- tests/playground/replica.tsx | 34 +-- tests/playground/replicaData.tsx | 170 ++++++----- vite.playground.config.ts | 14 +- vitest.config.ts | 10 +- wxt.config.ts | 32 +-- 49 files changed, 1380 insertions(+), 989 deletions(-) diff --git a/playwright.config.ts b/playwright.config.ts index ea2fa0f..03f858d 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,12 +1,12 @@ // playwright.config.ts -import { defineConfig } from '@playwright/test' +import { defineConfig } from "@playwright/test" export default defineConfig({ - reporter: [['html', { open: 'never' }]], - testDir: 'tests/e2e', + reporter: [["html", { open: "never" }]], + testDir: "tests/e2e", use: { - screenshot: 'only-on-failure', - trace: 'retain-on-failure', - video: 'retain-on-failure', + screenshot: "only-on-failure", + trace: "retain-on-failure", + video: "retain-on-failure", }, }) diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 478a947..48b6221 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -1,15 +1,15 @@ -import { type JSX, useState } from 'react' -import { twMerge } from 'tailwind-merge' -import type { VariantProps } from 'tailwind-variants' -import { badgeCVA, typeColors, typeIcons } from '@/components/design' -import type { CommentTableRow } from '@/entrypoints/background' +import { type JSX, useState } from "react" +import { twMerge } from "tailwind-merge" +import type { VariantProps } from "tailwind-variants" +import { badgeCVA, typeColors, typeIcons } from "@/components/design" +import type { CommentTableRow } from "@/entrypoints/background" -import { CodePreview } from './BadgePopups/CodePreview' -import { ImagePreview } from './BadgePopups/ImagePreview' -import { LinkPreview } from './BadgePopups/LinkPreview' -import { OpenTabPopup } from './BadgePopups/OpenTabPopup' -import { TextPreview } from './BadgePopups/TextPreview' -import { TimePreview } from './BadgePopups/TimePreview' +import { CodePreview } from "./BadgePopups/CodePreview" +import { ImagePreview } from "./BadgePopups/ImagePreview" +import { LinkPreview } from "./BadgePopups/LinkPreview" +import { OpenTabPopup } from "./BadgePopups/OpenTabPopup" +import { TextPreview } from "./BadgePopups/TextPreview" +import { TimePreview } from "./BadgePopups/TimePreview" const typePopups = { code: CodePreview, @@ -18,7 +18,9 @@ const typePopups = { open: OpenTabPopup, text: TextPreview, time: TimePreview, -} satisfies Partial JSX.Element>> +} satisfies Partial< + Record JSX.Element> +> export interface BadgePopupProps { row: CommentTableRow @@ -34,11 +36,13 @@ const Badge = ({ text, type, data }: BadgeProps) => { const Icon = typeIcons[type] const [showTooltip, setShowTooltip] = useState(false) const PopupComponent = - showTooltip && type in typePopups && typePopups[type as keyof typeof typePopups] + showTooltip && + type in typePopups && + typePopups[type as keyof typeof typePopups] return ( - - - diff --git a/src/components/CommentRow.tsx b/src/components/CommentRow.tsx index 5ffd695..d11811b 100644 --- a/src/components/CommentRow.tsx +++ b/src/components/CommentRow.tsx @@ -1,8 +1,8 @@ -import Badge from '@/components/Badge' -import { timeAgo } from '@/components/misc' -import type { CommentTableRow } from '@/entrypoints/background' -import { openOrFocusComment } from '@/entrypoints/popup/popup' -import { EnhancerRegistry } from '@/lib/registries' +import Badge from "@/components/Badge" +import { timeAgo } from "@/components/misc" +import type { CommentTableRow } from "@/entrypoints/background" +import { openOrFocusComment } from "@/entrypoints/popup/popup" +import { EnhancerRegistry } from "@/lib/registries" const enhancers = new EnhancerRegistry() @@ -14,60 +14,86 @@ type CommentRowProps = { handleTrash: (row: CommentTableRow) => void } -export function CommentRow({ row, selectedIds, toggleSelection }: CommentRowProps) { +export function CommentRow({ + row, + selectedIds, + toggleSelection, +}: CommentRowProps) { const enhancer = enhancers.enhancerFor(row.spot) const handleTitleClick = () => { openOrFocusComment(row.spot.unique_key) } return ( - - + + toggleSelection(row.spot.unique_key)} - className='rounded' + className="rounded" /> - -
+ +
{/* Context line */} -
-
+
+
{enhancer.tableUpperDecoration(row.spot)}
-
+
{row.latestDraft.stats.links.length > 0 && ( - + )} {row.latestDraft.stats.images.length > 0 && ( - + )} {row.latestDraft.stats.codeBlocks.length > 0 && ( - + )} - - - {row.isOpenTab && } + + + {row.isOpenTab && }
{/* Title */} -
+
- - {row.isTrashed && } + + {row.isTrashed && }
{/* Draft */} -
- {row.latestDraft.content.substring(0, 100)}… +
+ + {row.latestDraft.content.substring(0, 100)}… +
diff --git a/src/components/EmptyState.tsx b/src/components/EmptyState.tsx index 65814ef..a1b694a 100644 --- a/src/components/EmptyState.tsx +++ b/src/components/EmptyState.tsx @@ -1,17 +1,17 @@ export function EmptyState() { return ( -
-

No comments open

-

- Your drafts will appear here when you start typing in comment boxes across GitHub and - Reddit. +

+

No comments open

+

+ Your drafts will appear here when you start typing in comment boxes + across GitHub and Reddit.

-
- - · -
diff --git a/src/components/MultiSegment.tsx b/src/components/MultiSegment.tsx index 62cc078..e9fc0d8 100644 --- a/src/components/MultiSegment.tsx +++ b/src/components/MultiSegment.tsx @@ -1,4 +1,4 @@ -import { badgeCVA, typeIcons } from '@/components/design' +import { badgeCVA, typeIcons } from "@/components/design" interface Segment { text?: string @@ -11,9 +11,13 @@ interface MultiSegmentProps { onValueChange: (value: T) => void } -const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps) => { +const MultiSegment = ({ + segments, + value, + onValueChange, +}: MultiSegmentProps) => { return ( -
+
{segments.map((segment, index) => { const Icon = typeIcons[segment.type] const isFirst = index === 0 @@ -21,12 +25,12 @@ const MultiSegment = ({ segments, value, onValueChange }: MultiSegmentProps< const roundedClasses = isFirst && isLast - ? '' + ? "" : isFirst - ? '!rounded-r-none' + ? "!rounded-r-none" : isLast - ? '!rounded-l-none' - : '!rounded-none' + ? "!rounded-l-none" + : "!rounded-none" return ( ) diff --git a/src/components/NoMatchesState.tsx b/src/components/NoMatchesState.tsx index 0052595..9076bde 100644 --- a/src/components/NoMatchesState.tsx +++ b/src/components/NoMatchesState.tsx @@ -3,9 +3,13 @@ type NoMatchesStateProps = { } export function NoMatchesState({ onClearFilters }: NoMatchesStateProps) { return ( -
-

No matches found

-
diff --git a/src/components/PopupRoot.tsx b/src/components/PopupRoot.tsx index de75534..0725a49 100644 --- a/src/components/PopupRoot.tsx +++ b/src/components/PopupRoot.tsx @@ -1,19 +1,19 @@ -import { Eye, EyeOff, Search, Settings, Trash2 } from 'lucide-react' -import { useMemo, useState } from 'react' -import { twMerge } from 'tailwind-merge' -import { badgeCVA } from '@/components/design' -import MultiSegment from '@/components/MultiSegment' -import { allLeafValues } from '@/components/misc' -import type { CommentTableRow } from '@/entrypoints/background' -import type { FilterState } from '@/entrypoints/popup/popup' -import { BulkActionsBar } from './BulkActionsBar' -import { CommentRow } from './CommentRow' -import { EmptyState } from './EmptyState' -import { NoMatchesState } from './NoMatchesState' +import { Eye, EyeOff, Search, Settings, Trash2 } from "lucide-react" +import { useMemo, useState } from "react" +import { twMerge } from "tailwind-merge" +import { badgeCVA } from "@/components/design" +import MultiSegment from "@/components/MultiSegment" +import { allLeafValues } from "@/components/misc" +import type { CommentTableRow } from "@/entrypoints/background" +import type { FilterState } from "@/entrypoints/popup/popup" +import { BulkActionsBar } from "./BulkActionsBar" +import { CommentRow } from "./CommentRow" +import { EmptyState } from "./EmptyState" +import { NoMatchesState } from "./NoMatchesState" const initialFilter: FilterState = { - searchQuery: '', - sentFilter: 'both', + searchQuery: "", + sentFilter: "both", showTrashed: false, } @@ -25,7 +25,10 @@ export function PopupRoot({ drafts }: PopupRootProps) { const [selectedIds, setSelectedIds] = useState>(new Set()) const [filters, setFilters] = useState(initialFilter) - const updateFilter = (key: K, value: FilterState[K]) => { + const updateFilter = ( + key: K, + value: FilterState[K] + ) => { setFilters((prev) => ({ ...prev, [key]: value })) } @@ -34,8 +37,10 @@ export function PopupRoot({ drafts }: PopupRootProps) { if (!filters.showTrashed) { filtered = filtered.filter((d) => !d.isTrashed) } - if (filters.sentFilter !== 'both') { - filtered = filtered.filter((d) => (filters.sentFilter === 'sent' ? d.isSent : !d.isSent)) + if (filters.sentFilter !== "both") { + filtered = filtered.filter((d) => + filters.sentFilter === "sent" ? d.isSent : !d.isSent + ) } if (filters.searchQuery) { const query = filters.searchQuery.toLowerCase() @@ -64,7 +69,10 @@ export function PopupRoot({ drafts }: PopupRootProps) { } const toggleSelectAll = () => { - if (selectedIds.size === filteredDrafts.length && filteredDrafts.length > 0) { + if ( + selectedIds.size === filteredDrafts.length && + filteredDrafts.length > 0 + ) { setSelectedIds(new Set()) } else { setSelectedIds(new Set(filteredDrafts.map((d) => d.spot.unique_key))) @@ -72,23 +80,23 @@ export function PopupRoot({ drafts }: PopupRootProps) { } const handleOpen = (url: string) => { - window.open(url, '_blank') + window.open(url, "_blank") } const handleTrash = (row: CommentTableRow) => { if (row.latestDraft.stats.charCount > 20) { - if (confirm('Are you sure you want to discard this draft?')) { - console.log('Trashing draft:', row.spot.unique_key) + if (confirm("Are you sure you want to discard this draft?")) { + console.log("Trashing draft:", row.spot.unique_key) } } else { - console.log('Trashing draft:', row.spot.unique_key) + console.log("Trashing draft:", row.spot.unique_key) } } const clearFilters = () => { setFilters({ - searchQuery: '', - sentFilter: 'both', + searchQuery: "", + sentFilter: "both", showTrashed: true, }) } @@ -98,7 +106,10 @@ export function PopupRoot({ drafts }: PopupRootProps) { return } - if (filteredDrafts.length === 0 && (filters.searchQuery || filters.sentFilter !== 'both')) { + if ( + filteredDrafts.length === 0 && + (filters.searchQuery || filters.sentFilter !== "both") + ) { return } @@ -115,92 +126,106 @@ export function PopupRoot({ drafts }: PopupRootProps) { } return ( -
+
{/* Bulk actions bar - floating popup */} {selectedIds.size > 0 && } {/* Table */} -
- +
+
- + - + - - - {getTableBody()} + {getTableBody()}
+ 0} + type="checkbox" + checked={ + selectedIds.size === filteredDrafts.length && + filteredDrafts.length > 0 + } onChange={toggleSelectAll} - aria-label='Select all' - className='rounded' + aria-label="Select all" + className="rounded" /> -
-
-
- +
+
+
+
+ updateFilter('searchQuery', e.target.value)} - className='h-5 w-full rounded-sm border border-gray-300 pr-3 pl-5 font-normal text-sm focus:border-blue-500 focus:outline-none' + onChange={(e) => + updateFilter("searchQuery", e.target.value) + } + className="h-5 w-full rounded-sm border border-gray-300 pr-3 pl-5 font-normal text-sm focus:border-blue-500 focus:outline-none" />
-
+
- + value={filters.sentFilter} - onValueChange={(value) => updateFilter('sentFilter', value)} + onValueChange={(value) => + updateFilter("sentFilter", value) + } segments={[ { - text: '', - type: 'unsent', - value: 'unsent', + text: "", + type: "unsent", + value: "unsent", }, { - text: 'both', - type: 'blank', - value: 'both', + text: "both", + type: "blank", + value: "both", }, { - text: '', - type: 'sent', - value: 'sent', + text: "", + type: "sent", + value: "sent", }, ]} />
@@ -208,7 +233,7 @@ export function PopupRoot({ drafts }: PopupRootProps) {
diff --git a/src/components/design.tsx b/src/components/design.tsx index 082a570..36af986 100644 --- a/src/components/design.tsx +++ b/src/components/design.tsx @@ -10,8 +10,8 @@ import { Settings, TextSelect, Trash2, -} from 'lucide-react' -import { tv } from 'tailwind-variants' +} from "lucide-react" +import { tv } from "tailwind-variants" // Map types to their icons - source of truth for badge types export const typeIcons = { @@ -31,34 +31,34 @@ export const typeIcons = { // Type colors definition - must be exhaustive with typeIcons export const typeColors = { - blank: 'bg-transparent text-gray-700', - code: 'bg-pink-50 text-pink-700', - hideTrashed: 'bg-transparent text-gray-700', - image: 'bg-purple-50 text-purple-700', - link: 'bg-blue-50 text-blue-700', - open: 'bg-cyan-50 text-cyan-700', - sent: 'bg-green-50 text-green-700', - settings: 'bg-gray-50 text-gray-700', - text: 'bg-gray-50 text-gray-700', - time: 'bg-gray-50 text-gray-700', - trashed: 'bg-gray-50 text-yellow-700', - unsent: 'bg-amber-100 text-amber-700', + blank: "bg-transparent text-gray-700", + code: "bg-pink-50 text-pink-700", + hideTrashed: "bg-transparent text-gray-700", + image: "bg-purple-50 text-purple-700", + link: "bg-blue-50 text-blue-700", + open: "bg-cyan-50 text-cyan-700", + sent: "bg-green-50 text-green-700", + settings: "bg-gray-50 text-gray-700", + text: "bg-gray-50 text-gray-700", + time: "bg-gray-50 text-gray-700", + trashed: "bg-gray-50 text-yellow-700", + unsent: "bg-amber-100 text-amber-700", } as const satisfies Record // TV configuration for stat badges export const badgeCVA = tv({ - base: 'inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-normal h-5', + base: "inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-normal h-5", defaultVariants: { clickable: false, }, variants: { clickable: { - false: '', - true: 'cursor-pointer border border-transparent hover:border-current border-dashed', + false: "", + true: "cursor-pointer border border-transparent hover:border-current border-dashed", }, selected: { - false: '', - true: '!border-solid !border-current', + false: "", + true: "!border-solid !border-current", }, type: typeColors, }, diff --git a/src/components/misc.ts b/src/components/misc.ts index baddc6f..3bd01d7 100644 --- a/src/components/misc.ts +++ b/src/components/misc.ts @@ -1,28 +1,31 @@ export function timeAgo(date: Date | number): string { - const timestamp = typeof date === 'number' ? date : date.getTime() + const timestamp = typeof date === "number" ? date : date.getTime() const seconds = Math.floor((Date.now() - timestamp) / 1000) const intervals = [ - { label: 'y', secs: 31536000 }, - { label: 'mo', secs: 2592000 }, - { label: 'w', secs: 604800 }, - { label: 'd', secs: 86400 }, - { label: 'h', secs: 3600 }, - { label: 'm', secs: 60 }, - { label: 's', secs: 1 }, + { label: "y", secs: 31536000 }, + { label: "mo", secs: 2592000 }, + { label: "w", secs: 604800 }, + { label: "d", secs: 86400 }, + { label: "h", secs: 3600 }, + { label: "m", secs: 60 }, + { label: "s", secs: 1 }, ] for (const i of intervals) { const v = Math.floor(seconds / i.secs) if (v >= 1) return `${v}${i.label}` } - return 'just now' + return "just now" } /** Returns all leaf values of an arbitrary object as strings. */ -export function* allLeafValues(obj: any, visited = new Set()): Generator { +export function* allLeafValues( + obj: any, + visited = new Set() +): Generator { if (visited.has(obj) || obj == null) return - if (typeof obj === 'string') yield obj - else if (typeof obj === 'number') yield String(obj) - else if (typeof obj === 'object') { + if (typeof obj === "string") yield obj + else if (typeof obj === "number") yield String(obj) + else if (typeof obj === "object") { visited.add(obj) for (const key in obj) { yield* allLeafValues(obj[key], visited) diff --git a/src/entrypoints/background.ts b/src/entrypoints/background.ts index 8a28681..f9f96eb 100644 --- a/src/entrypoints/background.ts +++ b/src/entrypoints/background.ts @@ -1,14 +1,18 @@ -import type { CommentEvent, CommentEventType, CommentSpot } from '@/lib/enhancer' -import { type DraftStats, statsFor } from '@/lib/enhancers/draft-stats' -import { logger } from '@/lib/logger' -import type { GetTableRowsResponse, ToBackgroundMessage } from '@/lib/messages' +import type { + CommentEvent, + CommentEventType, + CommentSpot, +} from "@/lib/enhancer" +import { type DraftStats, statsFor } from "@/lib/enhancers/draft-stats" +import { logger } from "@/lib/logger" +import type { GetTableRowsResponse, ToBackgroundMessage } from "@/lib/messages" import { CLOSE_MESSAGE_PORT, isContentToBackgroundMessage, isGetOpenSpotsMessage, isOpenOrFocusMessage, KEEP_PORT_OPEN, -} from '@/lib/messages' +} from "@/lib/messages" export interface Tab { tabId: number @@ -36,8 +40,11 @@ export interface CommentTableRow { export const openSpots = new Map() export const openTabs = new Map>() -export function handleCommentEvent(message: CommentEvent, sender: any): boolean { - logger.debug('received comment event', message) +export function handleCommentEvent( + message: CommentEvent, + sender: any +): boolean { + logger.debug("received comment event", message) // Only process events with valid tab information if (!sender.tab?.id || !sender.tab?.windowId) { @@ -45,14 +52,14 @@ export function handleCommentEvent(message: CommentEvent, sender: any): boolean } switch (message.type) { - case 'ENHANCED': { + case "ENHANCED": { // track the draft const existingState = openSpots.get(message.spot.unique_key) if (existingState) { - existingState.drafts.push([Date.now(), message.draft || '']) + existingState.drafts.push([Date.now(), message.draft || ""]) } else { const commentState: CommentStorage = { - drafts: [[Date.now(), message.draft || '']], + drafts: [[Date.now(), message.draft || ""]], sentOn: null, spot: message.spot, trashedOn: null, @@ -72,15 +79,15 @@ export function handleCommentEvent(message: CommentEvent, sender: any): boolean } break } - case 'DESTROYED': { + case "DESTROYED": { openSpots.delete(message.spot.unique_key) break } - case 'LOST_FOCUS': { + case "LOST_FOCUS": { // Update the draft content for existing comment state const existingState = openSpots.get(message.spot.unique_key) if (existingState) { - existingState.drafts.push([Date.now(), message.draft || '']) + existingState.drafts.push([Date.now(), message.draft || ""]) } break } @@ -97,30 +104,32 @@ export function handleCommentEvent(message: CommentEvent, sender: any): boolean export function handlePopupMessage( message: any, _sender: any, - sendResponse: (response: any) => void, + sendResponse: (response: any) => void ): typeof CLOSE_MESSAGE_PORT | typeof KEEP_PORT_OPEN { if (isGetOpenSpotsMessage(message)) { - logger.debug('received open spots message', message) - const rows: CommentTableRow[] = Array.from(openSpots.values()).map((storage) => { - const [time, content] = storage.drafts.at(-1)! - const row: CommentTableRow = { - isOpenTab: true, - isSent: storage.sentOn != null, - isTrashed: storage.trashedOn != null, - latestDraft: { - content, - stats: statsFor(content), - time, - }, - spot: storage.spot, + logger.debug("received open spots message", message) + const rows: CommentTableRow[] = Array.from(openSpots.values()).map( + (storage) => { + const [time, content] = storage.drafts.at(-1)! + const row: CommentTableRow = { + isOpenTab: true, + isSent: storage.sentOn != null, + isTrashed: storage.trashedOn != null, + latestDraft: { + content, + stats: statsFor(content), + time, + }, + spot: storage.spot, + } + return row } - return row - }) + ) const response: GetTableRowsResponse = { rows } sendResponse(response) return KEEP_PORT_OPEN } else if (isOpenOrFocusMessage(message)) { - logger.debug('received switch tab message', message) + logger.debug("received switch tab message", message) const tabs = openTabs.get(message.uniqueKey) if (tabs) { browser.windows @@ -129,39 +138,49 @@ export function handlePopupMessage( return browser.tabs.update(tabs[0]!.tabId, { active: true }) }) .catch((error) => { - console.error('Error switching to tab:', error) + console.error("Error switching to tab:", error) }) } else { - console.error('TODO: implement opening a previous comment', message.uniqueKey) + console.error( + "TODO: implement opening a previous comment", + message.uniqueKey + ) } return CLOSE_MESSAGE_PORT } else { - logger.error('received unknown message', message) - throw new Error(`Unhandled popup message type: ${message?.type || 'unknown'}`) + logger.error("received unknown message", message) + throw new Error( + `Unhandled popup message type: ${message?.type || "unknown"}` + ) } } export default defineBackground(() => { - browser.runtime.onMessage.addListener((message: ToBackgroundMessage, sender, sendResponse) => { - if (isContentToBackgroundMessage(message)) { - return handleCommentEvent(message, sender) - } else { - return handlePopupMessage(message, sender, sendResponse) + browser.runtime.onMessage.addListener( + (message: ToBackgroundMessage, sender, sendResponse) => { + if (isContentToBackgroundMessage(message)) { + return handleCommentEvent(message, sender) + } else { + return handlePopupMessage(message, sender, sendResponse) + } } - }) + ) browser.tabs.onRemoved.addListener((closedTabId: number) => { - logger.debug('tab removed', { tabId: closedTabId }) + logger.debug("tab removed", { tabId: closedTabId }) // Clean up openSpots entries for the closed tab for (const [key, tabs] of openTabs) { const remainingTabs = tabs.filter((tab) => tab.tabId !== closedTabId) if (remainingTabs.length === 0) { - logger.debug('closed every tab which contained spot', key) + logger.debug("closed every tab which contained spot", key) openSpots.delete(key) openTabs.delete(key) } else if (remainingTabs.length < tabs.length) { - logger.debug('closed tab which contained a spot, other tabs still open', key) + logger.debug( + "closed tab which contained a spot, other tabs still open", + key + ) openTabs.set(key, remainingTabs) } } diff --git a/src/entrypoints/content.ts b/src/entrypoints/content.ts index 903f4b5..e173dcb 100644 --- a/src/entrypoints/content.ts +++ b/src/entrypoints/content.ts @@ -1,6 +1,6 @@ -import type { CommentEvent, StrippedLocation } from '../lib/enhancer' -import { logger } from '../lib/logger' -import { EnhancerRegistry, TextareaRegistry } from '../lib/registries' +import type { CommentEvent, StrippedLocation } from "../lib/enhancer" +import { logger } from "../lib/logger" +import { EnhancerRegistry, TextareaRegistry } from "../lib/registries" const enhancers = new EnhancerRegistry() const enhancedTextareas = new TextareaRegistry() @@ -16,13 +16,13 @@ function detectLocation(): StrippedLocation { host: window.location.host, pathname: window.location.pathname, } - logger.debug('[gitcasso] detectLocation called, returning:', result) + logger.debug("[gitcasso] detectLocation called, returning:", result) return result } function sendEventToBackground(message: CommentEvent): void { browser.runtime.sendMessage(message).catch((error) => { - logger.error('Failed to send event to background:', error) + logger.error("Failed to send event to background:", error) }) } @@ -30,8 +30,9 @@ enhancedTextareas.setCommentEventSender(sendEventToBackground) export default defineContentScript({ main() { - logger.debug('Main was called') - const textAreasOnPageLoad = document.querySelectorAll(`textarea`) + logger.debug("Main was called") + const textAreasOnPageLoad = + document.querySelectorAll(`textarea`) for (const textarea of textAreasOnPageLoad) { enhanceMaybe(textarea) } @@ -42,16 +43,20 @@ export default defineContentScript({ }) // Listen for tab visibility changes to capture draft content when switching tabs - document.addEventListener('visibilitychange', () => { + document.addEventListener("visibilitychange", () => { if (document.hidden) { enhancedTextareas.tabLostFocus() } }) - logger.debug('Extension loaded with', enhancers.getEnhancerCount(), 'handlers') + logger.debug( + "Extension loaded with", + enhancers.getEnhancerCount(), + "handlers" + ) }, - matches: [''], - runAt: 'document_end', + matches: [""], + runAt: "document_end", }) function handleMutations(mutations: MutationRecord[]): void { @@ -59,11 +64,11 @@ 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") { enhanceMaybe(element as HTMLTextAreaElement) } else { // Also check for textareas within added subtrees - const textareas = element.querySelectorAll?.('textarea') + const textareas = element.querySelectorAll?.("textarea") if (textareas) { for (const textarea of textareas) { enhanceMaybe(textarea) @@ -76,11 +81,13 @@ function handleMutations(mutations: MutationRecord[]): void { for (const node of mutation.removedNodes) { if (node.nodeType === Node.ELEMENT_NODE) { const element = node as Element - if (element.tagName === 'TEXTAREA') { - enhancedTextareas.unregisterDueToModification(element as HTMLTextAreaElement) + if (element.tagName === "TEXTAREA") { + enhancedTextareas.unregisterDueToModification( + element as HTMLTextAreaElement + ) } else { // Also check for textareas within removed subtrees - const textareas = element.querySelectorAll?.('textarea') + const textareas = element.querySelectorAll?.("textarea") if (textareas) { for (const textarea of textareas) { enhancedTextareas.unregisterDueToModification(textarea) @@ -93,24 +100,28 @@ function handleMutations(mutations: MutationRecord[]): void { } function enhanceMaybe(textarea: HTMLTextAreaElement) { - logger.debug('[gitcasso] enhanceMaybe called for textarea:', textarea.id, textarea.className) + logger.debug( + "[gitcasso] enhanceMaybe called for textarea:", + textarea.id, + textarea.className + ) if (enhancedTextareas.get(textarea)) { - logger.debug('textarea already registered {}', textarea) + logger.debug("textarea already registered {}", textarea) return } try { const location = detectLocation() - logger.debug('[gitcasso] Calling tryToEnhance with location:', location) + logger.debug("[gitcasso] Calling tryToEnhance with location:", location) const enhancedTextarea = enhancers.tryToEnhance(textarea, location) if (enhancedTextarea) { logger.debug( - 'Identified textarea:', + "Identified textarea:", enhancedTextarea.spot.type, - enhancedTextarea.spot.unique_key, + enhancedTextarea.spot.unique_key ) enhancedTextareas.register(enhancedTextarea) } else { - logger.debug('No handler found for textarea') + logger.debug("No handler found for textarea") } } catch (e) { logger.error(e) diff --git a/src/entrypoints/popup/popup.tsx b/src/entrypoints/popup/popup.tsx index 96d6e23..7b01444 100644 --- a/src/entrypoints/popup/popup.tsx +++ b/src/entrypoints/popup/popup.tsx @@ -1,39 +1,45 @@ -import './style.css' -import { createRoot } from 'react-dom/client' -import { PopupRoot } from '@/components/PopupRoot' -import type { CommentTableRow } from '@/entrypoints/background' -import { logger } from '@/lib/logger' -import type { GetOpenSpotsMessage, GetTableRowsResponse, OpenOrFocusMessage } from '@/lib/messages' +import "./style.css" +import { createRoot } from "react-dom/client" +import { PopupRoot } from "@/components/PopupRoot" +import type { CommentTableRow } from "@/entrypoints/background" +import { logger } from "@/lib/logger" +import type { + GetOpenSpotsMessage, + GetTableRowsResponse, + OpenOrFocusMessage, +} from "@/lib/messages" export interface FilterState { - sentFilter: 'both' | 'sent' | 'unsent' + sentFilter: "both" | "sent" | "unsent" searchQuery: string showTrashed: boolean } async function getOpenSpots(): Promise { - logger.debug('Sending message to background script...') + logger.debug("Sending message to background script...") try { - const message: GetOpenSpotsMessage = { type: 'GET_OPEN_SPOTS' } - const response = (await browser.runtime.sendMessage(message)) as GetTableRowsResponse - logger.debug('Received response:', response) + const message: GetOpenSpotsMessage = { type: "GET_OPEN_SPOTS" } + const response = (await browser.runtime.sendMessage( + message + )) as GetTableRowsResponse + logger.debug("Received response:", response) return response.rows } catch (error) { - logger.error('Error sending message to background:', error) + logger.error("Error sending message to background:", error) return [] } } export function openOrFocusComment(uniqueKey: string): void { const message: OpenOrFocusMessage = { - type: 'OPEN_OR_FOCUS_COMMENT', + type: "OPEN_OR_FOCUS_COMMENT", uniqueKey, } browser.runtime.sendMessage(message) window.close() } -const app = document.getElementById('app') +const app = document.getElementById("app") if (app) { const root = createRoot(app) @@ -43,9 +49,9 @@ if (app) { root.render() }) .catch((error) => { - logger.error('Failed to load initial data:', error) + logger.error("Failed to load initial data:", error) root.render() }) } else { - logger.error('App element not found') + logger.error("App element not found") } diff --git a/src/entrypoints/popup/style.css b/src/entrypoints/popup/style.css index 86e1bb3..7a0144e 100644 --- a/src/entrypoints/popup/style.css +++ b/src/entrypoints/popup/style.css @@ -9,7 +9,8 @@ body { width: var(--popup-width); height: var(--popup-height); - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; line-height: 1.4; margin: 0; diff --git a/src/lib/config.ts b/src/lib/config.ts index a98e375..1a1819a 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,13 +1,13 @@ -const MODES = ['PROD', 'PLAYGROUNDS_PR'] as const +const MODES = ["PROD", "PLAYGROUNDS_PR"] as const export type ModeType = (typeof MODES)[number] -const LOG_LEVELS = ['DEBUG', 'INFO', 'WARN', 'ERROR'] as const +const LOG_LEVELS = ["DEBUG", "INFO", "WARN", "ERROR"] as const export type LogLevel = (typeof LOG_LEVELS)[number] export const CONFIG = { - EXTENSION_NAME: 'gitcasso', // decorates logs - LOG_LEVEL: 'DEBUG' satisfies LogLevel, - MODE: 'PROD' satisfies ModeType, + EXTENSION_NAME: "gitcasso", // decorates logs + LOG_LEVEL: "DEBUG" satisfies LogLevel, + MODE: "PROD" satisfies ModeType, } as const diff --git a/src/lib/enhancer.ts b/src/lib/enhancer.ts index 56dc6a4..5f596da 100644 --- a/src/lib/enhancer.ts +++ b/src/lib/enhancer.ts @@ -1,5 +1,5 @@ -import type { OverTypeInstance } from 'overtype' -import type { ReactNode } from 'react' +import type { OverTypeInstance } from "overtype" +import type { ReactNode } from "react" /** * Stores enough info about the location of a draft to: @@ -11,7 +11,7 @@ export interface CommentSpot { type: string } -export type CommentEventType = 'ENHANCED' | 'LOST_FOCUS' | 'DESTROYED' +export type CommentEventType = "ENHANCED" | "LOST_FOCUS" | "DESTROYED" export interface CommentEvent { type: CommentEventType @@ -36,7 +36,10 @@ 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. */ - tryToEnhance(textarea: HTMLTextAreaElement, location: StrippedLocation): Spot | null + tryToEnhance( + textarea: HTMLTextAreaElement, + location: StrippedLocation + ): Spot | null /** * If `tryToEnhance` returns non-null, then this gets called. */ diff --git a/src/lib/enhancers/CommentEnhancerMissing.tsx b/src/lib/enhancers/CommentEnhancerMissing.tsx index b123e1c..80285c8 100644 --- a/src/lib/enhancers/CommentEnhancerMissing.tsx +++ b/src/lib/enhancers/CommentEnhancerMissing.tsx @@ -1,33 +1,37 @@ -import type { OverTypeInstance } from 'overtype' -import type { ReactNode } from 'react' -import type { CommentEnhancer, CommentSpot, StrippedLocation } from '../enhancer' +import type { OverTypeInstance } from "overtype" +import type { ReactNode } from "react" +import type { + CommentEnhancer, + CommentSpot, + StrippedLocation, +} from "../enhancer" /** Used when an entry is in the table which we don't recognize. */ export class CommentEnhancerMissing implements CommentEnhancer { tableUpperDecoration(spot: CommentSpot): ReactNode { return ( - - -
)} {/* Table */} -
- +
+
- + - + - - - {getTableBody()} + {getTableBody()}
+ 0} + type="checkbox" + checked={ + selectedIds.size === filteredDrafts.length && + filteredDrafts.length > 0 + } onChange={toggleSelectAll} - aria-label='Select all' - className='rounded' + aria-label="Select all" + className="rounded" /> -
-
-
- +
+
+
+
+ updateFilter('searchQuery', e.target.value)} - className='h-5 w-full rounded-sm border border-gray-300 pr-3 pl-5 font-normal text-sm focus:border-blue-500 focus:outline-none' + onChange={(e) => + updateFilter("searchQuery", e.target.value) + } + className="h-5 w-full rounded-sm border border-gray-300 pr-3 pl-5 font-normal text-sm focus:border-blue-500 focus:outline-none" />
-
+
- + value={filters.sentFilter} - onValueChange={(value) => updateFilter('sentFilter', value)} + onValueChange={(value) => + updateFilter("sentFilter", value) + } segments={[ { - text: '', - type: 'unsent', - value: 'unsent', + text: "", + type: "unsent", + value: "unsent", }, { - text: 'both', - type: 'blank', - value: 'both', + text: "both", + type: "blank", + value: "both", }, { - text: '', - type: 'sent', - value: 'sent', + text: "", + type: "sent", + value: "sent", }, ]} />
@@ -206,7 +243,7 @@ export const ClaudePrototype = () => {
@@ -219,53 +256,64 @@ function commentRow( selectedIds: Set, toggleSelection: (id: string) => void, _handleOpen: (url: string) => void, - _handleTrash: (row: CommentTableRow) => void, + _handleTrash: (row: CommentTableRow) => void ) { const enhancer = enhancers.enhancerFor(row.spot) return ( - - + + toggleSelection(row.spot.unique_key)} - className='rounded' + className="rounded" /> - -
+ +
{/* Context line */} -
-
+
+
{enhancer.tableUpperDecoration(row.spot)}
-
+
{row.latestDraft.stats.links.length > 0 && ( - + )} {row.latestDraft.stats.images.length > 0 && ( - + )} {row.latestDraft.stats.codeBlocks.length > 0 && ( - + )} - - - {row.isOpenTab && } + + + {row.isOpenTab && }
{/* Title */} -
- +
+ {enhancer.tableTitle(row.spot)} - - {row.isTrashed && } + + {row.isTrashed && }
{/* Draft */} -
- {row.latestDraft.content.substring(0, 100)}… +
+ + {row.latestDraft.content.substring(0, 100)}… +
@@ -274,17 +322,18 @@ function commentRow( } const EmptyState = () => ( -
-

No comments open

-

- Your drafts will appear here when you start typing in comment boxes across GitHub and Reddit. +

+

No comments open

+

+ Your drafts will appear here when you start typing in comment boxes across + GitHub and Reddit.

-
- - · -
@@ -292,9 +341,13 @@ const EmptyState = () => ( ) const NoMatchesState = ({ onClearFilters }: { onClearFilters: () => void }) => ( -
-

No matches found

-
diff --git a/tests/playground/playground.tsx b/tests/playground/playground.tsx index ea2b8b0..dba26a6 100644 --- a/tests/playground/playground.tsx +++ b/tests/playground/playground.tsx @@ -1,40 +1,45 @@ -import { useState } from 'react' -import { createRoot } from 'react-dom/client' -import '@/entrypoints/popup/style.css' -import './playground-styles.css' -import { ClaudePrototype } from './claude' -import { Replica } from './replica' +import { useState } from "react" +import { createRoot } from "react-dom/client" +import "@/entrypoints/popup/style.css" +import "./playground-styles.css" +import { ClaudePrototype } from "./claude" +import { Replica } from "./replica" const MODES = { - claude: { component: ClaudePrototype, label: 'claude' }, - replica: { component: Replica, label: 'replica' }, + claude: { component: ClaudePrototype, label: "claude" }, + replica: { component: Replica, label: "replica" }, } as const type Mode = keyof typeof MODES const App = () => { - const [activeComponent, setActiveComponent] = useState('claude') + const [activeComponent, setActiveComponent] = useState("claude") const ModeComponent = MODES[activeComponent].component return ( -
-
-
-

Popup Simulator

-
    -
  • The popup frame is meant to exactly match the browser extension popup.
  • +
    +
    +
    +

    + Popup Simulator +

    +
      +
    • + The popup frame is meant to exactly match the browser extension + popup. +
    • Hot reload is active for instant updates
    -
    +
    {Object.entries(MODES).map(([mode, config]) => (
    -
    +
    @@ -51,5 +56,5 @@ const App = () => { ) } -const root = createRoot(document.getElementById('root')!) +const root = createRoot(document.getElementById("root")!) root.render() diff --git a/tests/playground/replica.tsx b/tests/playground/replica.tsx index db019ad..f866587 100644 --- a/tests/playground/replica.tsx +++ b/tests/playground/replica.tsx @@ -1,30 +1,30 @@ -import { PopupRoot } from '@/components/PopupRoot' -import type { CommentStorage, CommentTableRow } from '@/entrypoints/background' -import type { CommentSpot } from '@/lib/enhancer' -import type { GitHubIssueAppendSpot } from '@/lib/enhancers/github/GitHubIssueAppendEnhancer' -import type { GitHubPrAppendSpot } from '@/lib/enhancers/github/GitHubPrAppendEnhancer' +import { PopupRoot } from "@/components/PopupRoot" +import type { CommentStorage, CommentTableRow } from "@/entrypoints/background" +import type { CommentSpot } from "@/lib/enhancer" +import type { GitHubIssueAppendSpot } from "@/lib/enhancers/github/GitHubIssueAppendEnhancer" +import type { GitHubPrAppendSpot } from "@/lib/enhancers/github/GitHubPrAppendEnhancer" const gh_pr: GitHubPrAppendSpot = { - domain: 'github.com', + domain: "github.com", number: 517, - slug: 'diffplug/selfie', - title: 'wowza', - type: 'GH_PR_APPEND', - unique_key: 'github.com:diffplug/selfie:517', + slug: "diffplug/selfie", + title: "wowza", + type: "GH_PR_APPEND", + unique_key: "github.com:diffplug/selfie:517", } const gh_issue: GitHubIssueAppendSpot = { - domain: 'github.com', + domain: "github.com", number: 523, - slug: 'diffplug/selfie', - title: 'whoa', - type: 'GH_ISSUE_APPEND', - unique_key: 'github.com:diffplug/selfie:523', + slug: "diffplug/selfie", + title: "whoa", + type: "GH_ISSUE_APPEND", + unique_key: "github.com:diffplug/selfie:523", } const spots: CommentSpot[] = [gh_pr, gh_issue] const sampleSpots: CommentStorage[] = spots.map((spot) => { const state: CommentStorage = { - drafts: [[0, 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.']], + drafts: [[0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit."]], sentOn: null, spot, trashedOn: null, @@ -41,7 +41,7 @@ export function Replica() { isSent: true, isTrashed: false, latestDraft: { - content: 'lorum ipsum', + content: "lorum ipsum", stats: { charCount: 99, codeBlocks: [], diff --git a/tests/playground/replicaData.tsx b/tests/playground/replicaData.tsx index 377d828..4a59eed 100644 --- a/tests/playground/replicaData.tsx +++ b/tests/playground/replicaData.tsx @@ -1,12 +1,12 @@ -import type { CommentTableRow } from '@/entrypoints/background' -import type { CommentSpot } from '@/lib/enhancer' -import type { GitHubIssueAppendSpot } from '@/lib/enhancers/github/GitHubIssueAppendEnhancer' -import type { GitHubPrAppendSpot } from '@/lib/enhancers/github/GitHubPrAppendEnhancer' +import type { CommentTableRow } from "@/entrypoints/background" +import type { CommentSpot } from "@/lib/enhancer" +import type { GitHubIssueAppendSpot } from "@/lib/enhancers/github/GitHubIssueAppendEnhancer" +import type { GitHubPrAppendSpot } from "@/lib/enhancers/github/GitHubPrAppendEnhancer" export interface RedditSpot extends CommentSpot { title: string subreddit: string - type: 'REDDIT' + type: "REDDIT" } const withSpot = (spot: T): T => spot @@ -18,32 +18,36 @@ export const generateMockDrafts = (): CommentTableRow[] => [ isTrashed: false, latestDraft: { content: - 'This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...', + "This PR addresses the memory leak issue reported in #1233. The problem was caused by event listeners not being properly disposed...", stats: { charCount: 245, codeBlocks: [ - { code: 'const listener = () => {}', language: 'typescript' }, - { code: 'element.removeEventListener()', language: 'javascript' }, - { code: 'dispose()', language: 'typescript' }, + { code: "const listener = () => {}", language: "typescript" }, + { code: "element.removeEventListener()", language: "javascript" }, + { code: "dispose()", language: "typescript" }, ], images: [ - { url: 'https://example.com/image1.png' }, - { url: 'https://example.com/image2.png' }, + { url: "https://example.com/image1.png" }, + { url: "https://example.com/image2.png" }, ], links: [ - { text: 'Issue #1233', url: 'https://github.com/microsoft/vscode/issues/1233' }, - { text: 'Documentation', url: 'https://docs.microsoft.com' }, + { + text: "Issue #1233", + url: "https://github.com/microsoft/vscode/issues/1233", + }, + { text: "Documentation", url: "https://docs.microsoft.com" }, ], }, time: Date.now() - 1000 * 60 * 30, }, spot: withSpot({ - domain: 'github.com', + domain: "github.com", number: 1234, - slug: 'microsoft/vscode', - title: "Fix memory leak in extension host (why is this so hard! It's been months!)", - type: 'GH_PR_APPEND', - unique_key: '1', + slug: "microsoft/vscode", + title: + "Fix memory leak in extension host (why is this so hard! It's been months!)", + type: "GH_PR_APPEND", + unique_key: "1", } satisfies GitHubPrAppendSpot), }, { @@ -59,18 +63,18 @@ export const generateMockDrafts = (): CommentTableRow[] => [ images: [], links: [ { - text: 'GitLens', - url: 'https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens', + text: "GitLens", + url: "https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens", }, ], }, time: Date.now() - 1000 * 60 * 60 * 2, }, spot: withSpot({ - subreddit: 'programming', + subreddit: "programming", title: "Re: What's your favorite VS Code extension?", - type: 'REDDIT', - unique_key: '2', + type: "REDDIT", + unique_key: "2", } satisfies RedditSpot), }, { @@ -82,19 +86,24 @@ export const generateMockDrafts = (): CommentTableRow[] => [ "When using useEffect with async functions, the cleanup function doesn't seem to be called correctly in certain edge cases...", stats: { charCount: 456, - codeBlocks: [{ code: 'useEffect(() => { /* async code */ }, [])', language: 'javascript' }], + codeBlocks: [ + { + code: "useEffect(() => { /* async code */ }, [])", + language: "javascript", + }, + ], images: [], links: [], }, time: Date.now() - 1000 * 60 * 60 * 5, }, spot: withSpot({ - domain: 'github.com', + domain: "github.com", number: 5678, - slug: 'facebook/react', - title: 'Unexpected behavior with useEffect cleanup', - type: 'GH_ISSUE_APPEND', - unique_key: '3', + slug: "facebook/react", + title: "Unexpected behavior with useEffect cleanup", + type: "GH_ISSUE_APPEND", + unique_key: "3", } satisfies GitHubIssueAppendSpot), }, { @@ -103,34 +112,37 @@ export const generateMockDrafts = (): CommentTableRow[] => [ isTrashed: false, latestDraft: { content: - 'LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...', + "LGTM! Just a few minor suggestions about the examples in the routing section. Consider adding more context about...", stats: { charCount: 322, codeBlocks: [], images: [ - { url: 'routing-diagram.png' }, - { url: 'example-1.png' }, - { url: 'example-2.png' }, - { url: 'architecture.png' }, + { url: "routing-diagram.png" }, + { url: "example-1.png" }, + { url: "example-2.png" }, + { url: "architecture.png" }, ], links: [ - { text: 'Routing docs', url: 'https://nextjs.org/docs/routing' }, - { text: 'Examples', url: 'https://github.com/vercel/next.js/tree/main/examples' }, + { text: "Routing docs", url: "https://nextjs.org/docs/routing" }, + { + text: "Examples", + url: "https://github.com/vercel/next.js/tree/main/examples", + }, { - text: 'Migration guide', - url: 'https://nextjs.org/docs/app/building-your-application/upgrading', + text: "Migration guide", + url: "https://nextjs.org/docs/app/building-your-application/upgrading", }, ], }, time: Date.now() - 1000 * 60 * 60 * 24, }, spot: withSpot({ - domain: 'github', + domain: "github", number: 9012, - slug: 'vercel/next.js', - title: 'Update routing documentation', - type: 'GH_PR_APPEND', - unique_key: '4', + slug: "vercel/next.js", + title: "Update routing documentation", + type: "GH_PR_APPEND", + unique_key: "4", } satisfies GitHubPrAppendSpot), }, { @@ -139,39 +151,71 @@ export const generateMockDrafts = (): CommentTableRow[] => [ isTrashed: true, latestDraft: { content: - 'This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...', + "This PR implements ESM support in worker threads as discussed in the last TSC meeting. The implementation follows...", stats: { charCount: 678, codeBlocks: [ - { code: 'import { Worker } from "worker_threads"', language: 'javascript' }, - { code: 'new Worker("./worker.mjs", { type: "module" })', language: 'javascript' }, - { code: 'import { parentPort } from "worker_threads"', language: 'javascript' }, - { code: 'interface WorkerOptions { type: "module" }', language: 'typescript' }, - { code: 'await import("./dynamic-module.mjs")', language: 'javascript' }, - { code: 'export default function workerTask() {}', language: 'javascript' }, - { code: 'const result = await workerPromise', language: 'javascript' }, + { + code: 'import { Worker } from "worker_threads"', + language: "javascript", + }, + { + code: 'new Worker("./worker.mjs", { type: "module" })', + language: "javascript", + }, + { + code: 'import { parentPort } from "worker_threads"', + language: "javascript", + }, + { + code: 'interface WorkerOptions { type: "module" }', + language: "typescript", + }, + { + code: 'await import("./dynamic-module.mjs")', + language: "javascript", + }, + { + code: "export default function workerTask() {}", + language: "javascript", + }, + { + code: "const result = await workerPromise", + language: "javascript", + }, + ], + images: [ + { alt: "ESM Worker Architecture", url: "worker-architecture.png" }, ], - images: [{ alt: 'ESM Worker Architecture', url: 'worker-architecture.png' }], links: [ { - text: 'TSC Meeting Notes', - url: 'https://github.com/nodejs/TSC/blob/main/meetings/2023-11-01.md', + text: "TSC Meeting Notes", + url: "https://github.com/nodejs/TSC/blob/main/meetings/2023-11-01.md", + }, + { text: "ESM Spec", url: "https://tc39.es/ecma262/" }, + { + text: "Worker Threads docs", + url: "https://nodejs.org/api/worker_threads.html", + }, + { + text: "Implementation guide", + url: "https://nodejs.org/api/esm.html", + }, + { + text: "Related issue", + url: "https://github.com/nodejs/node/issues/30682", }, - { text: 'ESM Spec', url: 'https://tc39.es/ecma262/' }, - { text: 'Worker Threads docs', url: 'https://nodejs.org/api/worker_threads.html' }, - { text: 'Implementation guide', url: 'https://nodejs.org/api/esm.html' }, - { text: 'Related issue', url: 'https://github.com/nodejs/node/issues/30682' }, ], }, time: Date.now() - 1000 * 60 * 60 * 48, }, spot: withSpot({ - domain: 'github.com', + domain: "github.com", number: 3456, - slug: 'nodejs/node', - title: 'Add support for ESM in worker threads', - type: 'GH_PR_APPEND', - unique_key: '5', + slug: "nodejs/node", + title: "Add support for ESM in worker threads", + type: "GH_PR_APPEND", + unique_key: "5", } satisfies GitHubPrAppendSpot), }, ] diff --git a/vite.playground.config.ts b/vite.playground.config.ts index 73ff163..136519c 100644 --- a/vite.playground.config.ts +++ b/vite.playground.config.ts @@ -1,20 +1,20 @@ -import path from 'node:path' -import tailwindcss from '@tailwindcss/vite' -import react from '@vitejs/plugin-react' -import { defineConfig } from 'vite' +import path from "node:path" +import tailwindcss from "@tailwindcss/vite" +import react from "@vitejs/plugin-react" +import { defineConfig } from "vite" export default defineConfig({ build: { emptyOutDir: true, - outDir: '../../dist-playground', + outDir: "../../dist-playground", }, plugins: [react(), tailwindcss()], resolve: { alias: { - '@': path.resolve('./src'), + "@": path.resolve("./src"), }, }, - root: 'tests/playground', + root: "tests/playground", server: { host: true, open: true, diff --git a/vitest.config.ts b/vitest.config.ts index 71ca1ff..438fcb4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,12 +1,12 @@ -import { defineConfig } from 'vitest/config' -import { WxtVitest } from 'wxt/testing' +import { defineConfig } from "vitest/config" +import { WxtVitest } from "wxt/testing" export default defineConfig({ plugins: [WxtVitest()], test: { - environment: 'node', + environment: "node", globals: true, - include: ['tests/**/*.test.{ts,tsx}'], - pool: 'threads', + include: ["tests/**/*.test.{ts,tsx}"], + pool: "threads", }, }) diff --git a/wxt.config.ts b/wxt.config.ts index 354575e..086bd06 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -1,30 +1,30 @@ -import path from 'node:path' -import tailwindcss from '@tailwindcss/vite' -import react from '@vitejs/plugin-react' -import { defineConfig } from 'wxt' +import path from "node:path" +import tailwindcss from "@tailwindcss/vite" +import react from "@vitejs/plugin-react" +import { defineConfig } from "wxt" export default defineConfig({ manifest: { description: - 'Syntax highlighting and autosave for comments on GitHub (and other markdown-friendly websites).', - host_permissions: ['https://github.com/*'], + "Syntax highlighting and autosave for comments on GitHub (and other markdown-friendly websites).", + host_permissions: ["https://github.com/*"], icons: { - 16: '/icons/icon-16.png', - 48: '/icons/icon-48.png', - 128: '/icons/icon-128.png', + 16: "/icons/icon-16.png", + 48: "/icons/icon-48.png", + 128: "/icons/icon-128.png", }, - name: 'Gitcasso', - optional_host_permissions: ['https://*/*', 'http://*/*'], - permissions: ['activeTab', 'tabs'], - version: '0.1.0', + name: "Gitcasso", + optional_host_permissions: ["https://*/*", "http://*/*"], + permissions: ["activeTab", "tabs"], + version: "0.1.0", }, - modules: ['@wxt-dev/webextension-polyfill'], - srcDir: 'src', + modules: ["@wxt-dev/webextension-polyfill"], + srcDir: "src", vite: () => ({ plugins: [react(), tailwindcss()], resolve: { alias: { - '@': path.resolve('./src'), + "@": path.resolve("./src"), }, }, }), From 69bdfaa1fd380bb3ee3b61fa6a63504edb218115 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 29 Sep 2025 10:07:35 -0700 Subject: [PATCH 4/4] Disable `useSortedKeys`. --- biome.json | 8 -------- wxt.config.ts | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/biome.json b/biome.json index 9f1d661..8790e17 100644 --- a/biome.json +++ b/biome.json @@ -1,13 +1,5 @@ { "$schema": "https://biomejs.dev/schemas/2.2.3/schema.json", - "assist": { - "actions": { - "source": { - "useSortedKeys": "on" - } - }, - "enabled": true - }, "files": { "ignoreUnknown": false, "includes": [".*", "*.config.ts", "src/**", "tests/**", "!src/overtype"] diff --git a/wxt.config.ts b/wxt.config.ts index 086bd06..2802cac 100644 --- a/wxt.config.ts +++ b/wxt.config.ts @@ -8,13 +8,13 @@ export default defineConfig({ description: "Syntax highlighting and autosave for comments on GitHub (and other markdown-friendly websites).", host_permissions: ["https://github.com/*"], + optional_host_permissions: ["https://*/*", "http://*/*"], icons: { 16: "/icons/icon-16.png", 48: "/icons/icon-48.png", 128: "/icons/icon-128.png", }, name: "Gitcasso", - optional_host_permissions: ["https://*/*", "http://*/*"], permissions: ["activeTab", "tabs"], version: "0.1.0", },