Skip to content

feat(extension): add Vapor QA Chrome extension#623

Open
MaxLee-dev wants to merge 10 commits into
mainfrom
feature/vapor-155-4-핵심-개발

Hidden character warning

The head ref may contain hidden characters: "feature/vapor-155-4-\ud575\uc2ec-\uac1c\ubc1c"
Open

feat(extension): add Vapor QA Chrome extension#623
MaxLee-dev wants to merge 10 commits into
mainfrom
feature/vapor-155-4-핵심-개발

Conversation

@MaxLee-dev

@MaxLee-dev MaxLee-dev commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Related Issues

  • VAPOR-155 (4. 핵심 개발)

Description of Changes

Implements Vapor QA, a Chrome (WXT + React) extension for visual design QA: pin UI elements on any page, leave inline memos, and file them as a single Linear issue. This is the initial feature drop for the QA extension under apps/extension/.

The extension is split across four execution contexts that communicate via @webext-core/messaging:

Context Responsibility
popup API key entry · inspector on/off toggle · session reset
sidepanel Review collected items · register them to Linear
background Service worker: captureVisibleTab · IndexedDB image store · message routing
content script inspector.content/ — overlay/memo/capture (isolated world) + React Fiber ancestry reading (MAIN world via fiber-reader.ts)

Key capabilities:

  • Element inspector — hover highlight, click-to-pin, inline memo bubble, stable CSS selector + core computed-style extraction.
  • Component ancestry — reads the React Fiber tree in the MAIN world to surface up to 3 nearest named components per pinned element (vendor-neutral; structural *Context/*Provider wrappers excluded).
  • Screenshot capture + sharing — captures the visible tab in the background context (origin-matched with the sidepanel so it can read the image), sharing one image across items in the same tabId/pageUrl/scrollX/scrollY/width viewport.
  • Linear integration — composes a single issue with numbered annotated screenshots (boxes + labels baked in, embedded as base64 data URLs) and per-item memo/selector/components, fitting Linear's description length limit.

A few non-obvious architectural decisions are documented in apps/extension/CLAUDE.md (capture lives in background for origin reasons; Fiber is read only in MAIN world; inspecting state is a single in-memory variable, so a page reload silently turns it off — by design).

Screenshots

Checklist

  • The PR title follows the Conventional Commits convention.
  • I have added tests for my changes. (Unit tests across utils/ and content-script helpers — vitest.)
  • I have updated the Storybook or relevant documentation. (apps/extension/CLAUDE.md + docs/.)
  • I have added a changeset for this change.
  • I have performed a self-code review.
  • I have followed the project's coding conventions and component patterns.

Summary by CodeRabbit

  • New Features

    • Added a browser extension workflow for inspecting pages, pinning elements, leaving memos, capturing screenshots, and reviewing items in a side panel.
    • Added a popup with API key setup, inspection status controls, and session reset actions.
    • Added a lightbox view for captured images with overlay annotations.
  • Bug Fixes

    • Improved image sizing, overlay positioning, and panel state behavior for a smoother review experience.
    • Added cleanup handling so saved items and images stay in sync after sharing to Linear.

MaxLee-dev and others added 8 commits June 24, 2026 17:32
WXT가 자동 생성한 ~/* alias(extension 루트 기준)가 있었으나 코드는
전부 ../../utils/... 상대경로를 써왔다. 디렉토리 경계를 넘는 ../ import을
~/utils/...로 전환해 상대경로 깊이 계산을 제거. ./ 형제 import은 유지.

vitest는 .wxt/tsconfig.json의 alias를 읽지 못하므로 WxtVitest 플러그인을
꽂은 vitest.config.ts를 신설 — 테스트의 크로스 import도 동일하게 resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Adjusted memo bubble positioning to ensure it stays within viewport bounds.
- Added unit tests for MemoBubble component to verify correct positioning behavior.
- Updated ApiKeyForm and App components to use consistent padding styles.
- Introduced RegisterBar tests to validate error handling and issue creation flow.
- Enhanced session store to include tabId and page information for QA items.
- Refactored image sharing logic to prevent reuse across different tabs and pages.
- Improved messaging protocol to handle fiber responses more robustly.
- Updated wxt configuration to require activeTab permission for better functionality.
@MaxLee-dev MaxLee-dev requested a review from noahchoii as a code owner June 26, 2026 00:41
@changeset-bot

changeset-bot Bot commented Jun 26, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 0bb09ce

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
vapor-ui Ready Ready Preview, Comment Jun 26, 2026 1:21am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@MaxLee-dev, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 22 minutes and 5 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2dbc34fb-834a-4ece-9ddc-5d441b8a18e1

📥 Commits

Reviewing files that changed from the base of the PR and between 38c3613 and 0bb09ce.

📒 Files selected for processing (11)
  • apps/extension/entrypoints/fiber-reader.ts
  • apps/extension/entrypoints/inspector.content/index.ts
  • apps/extension/entrypoints/inspector.content/inspector-ui.tsx
  • apps/extension/entrypoints/inspector.content/lightbox.ts
  • apps/extension/entrypoints/inspector.content/memo-bubble.test.tsx
  • apps/extension/entrypoints/popup/useApiKey.ts
  • apps/extension/entrypoints/sidepanel/ItemCard.tsx
  • apps/extension/entrypoints/sidepanel/RegisterBar.tsx
  • apps/extension/utils/data/image-store.ts
  • apps/extension/utils/data/session-store.ts
  • apps/extension/utils/linear/image-sharing.ts
📝 Walkthrough

Walkthrough

The PR adds an extension-based QA capture and review flow with shared storage and messaging, DOM and React Fiber utilities, Linear issue builders, background capture and lightbox handling, and popup and sidepanel screens.

Changes

Extension QA workflow

Layer / File(s) Summary
Workspace setup
.prettierignore, apps/extension/.gitignore, apps/extension/CLAUDE.md, apps/extension/package.json, apps/extension/tsconfig.json, apps/extension/eslint.config.mjs, apps/extension/vitest.config.ts, apps/extension/wxt.config.ts, turbo.json
Extension ignore rules, repository guidance, package metadata, and WXT/TypeScript/ESLint/Vitest/Turbo config files are added or updated.
Shared stores and messaging
apps/extension/utils/browser/active-tab.ts, apps/extension/utils/data/*, apps/extension/utils/messaging/*
Shared session/image stores, extension messaging contracts, active-tab lookup, and protocol/store tests are added.
DOM capture utilities
apps/extension/utils/dom/*
DOM selector, style extraction, image annotation, and React Fiber ancestry helpers are added with tests.
Linear issue assembly
apps/extension/utils/linear/*
The Linear GraphQL client, image grouping and reuse helpers, and issue title and description builders are added with tests.
Background controller
apps/extension/entrypoints/background.ts, apps/extension/utils/browser/background.test.ts
Background side-panel state handling, capture selection, and lightbox dispatch are added with background tests.
Inspector runtime
apps/extension/entrypoints/fiber-reader.ts, apps/extension/entrypoints/inspector.content/*
The fiber bridge, inspector overlay and memo UI, lightbox rendering, and content-script orchestration are added with tests.
Popup workflow
apps/extension/entrypoints/popup/*
Popup API-key, inspecting, and session hooks are added with the popup shell and gating UI.
Sidepanel workflow
apps/extension/entrypoints/sidepanel/*
Sidepanel item grouping, registration, and review UI are added with the register bar test and sidepanel shell.

Sequence Diagram(s)

sequenceDiagram
  participant InspectorUi
  participant InspectorContent as "inspector.content/index.ts"
  participant Background as "background.ts"
  participant SessionStore as "session-store"
  participant ImageStore as "image-store"
  InspectorUi->>InspectorContent: onSaveMemo(memo)
  InspectorContent->>Background: sendMessage('captureAndStore', ...)
  Background->>SessionStore: getItems()
  alt shared capture exists
    Background-->>InspectorContent: { imageRef, index, tabId }
  else new capture
    Background->>ImageStore: putImage(dataUrlToBlob(...))
    Background-->>InspectorContent: { imageRef, index: 1, tabId }
  end
  InspectorContent->>SessionStore: addItem({...})
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • goorm-dev/vapor-ui#572: Also adjusts .prettierignore entries for generated paths, which overlaps with the ignore-file updates in this PR.

Suggested labels

scope: all

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main change: adding the Vapor QA Chrome extension.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/vapor-155-4-핵심-개발

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (5)
apps/extension/package.json (1)

8-8: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove redundant compile script.

compile duplicates typecheck (both run tsc --noEmit). Having two aliases for the same command risks confusion about which to use in CI/docs. Keep typecheck as the canonical name per CLAUDE.md.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/extension/package.json` at line 8, The package.json scripts include a
redundant compile alias that runs the same tsc --noEmit command as typecheck, so
remove the compile entry and keep typecheck as the single canonical script name.
Update the scripts block in package.json to avoid duplicate aliases and ensure
any references align with typecheck.
apps/extension/utils/data/image-store.test.ts (1)

11-24: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add one automated round-trip test for the IndexedDB path.

The current suite only exercises pure conversions, so a regression in putImage / getImage / clearImages would surface only in the integrated extension flow. Covering one real persistence round trip here would materially increase confidence in this module’s core behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/extension/utils/data/image-store.test.ts` around lines 11 - 24, The
current tests only verify conversion helpers, so add one automated IndexedDB
round-trip test that exercises the real persistence flow through putImage,
getImage, and clearImages in image-store.test.ts. Use the existing image-store
APIs to store a Blob or data URL, retrieve it back, assert the persisted payload
matches, and then clear it to confirm cleanup works. Keep the new test aligned
with the existing helper-focused style, but make it cover the actual IDB-backed
path end to end.
apps/extension/utils/linear/build-issue.test.ts (1)

92-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that head.width is forwarded to annotateImage.

These tests prove the data-URL/no-upload path, but not the scaling contract. If buildDescription() stops passing head.width, Linear screenshots regress to misaligned boxes and this suite still stays green.

Suggested assertion
         const description = await buildDescription(
             [item({ imageRef: 'image-1', width: 1200 })],
             {},
         );

+        expect(mocks.annotateImage).toHaveBeenCalledWith(
+            expect.any(Blob),
+            expect.any(Array),
+            1200,
+        );
         expect(mocks.uploadImage).not.toHaveBeenCalled();
         expect(description).toContain('![screenshot](data:image/png;base64,annotated)');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/extension/utils/linear/build-issue.test.ts` around lines 92 - 106, The
`buildDescription()` test covers the no-upload data-URL path but does not verify
the scaling input passed into `annotateImage`. Update the test around
`buildDescription`, `mocks.annotateImage`, and the `item({ imageRef, width })`
setup to assert that the `head.width` value is forwarded as the width argument
when `annotateImage` is called. Keep the existing no-upload assertions and add a
check on the `annotateImage` mock call so this contract cannot regress silently.
apps/extension/utils/dom/style-extract.test.ts (1)

13-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert computed values too.

This only checks key presence/count, so extractStyle() could return empty strings for every property and still pass. Add at least the color and font-size assertions from the setup.

Suggested test tightening
         const style = extractStyle(el);

+        expect(style.color).toBe('rgb(255, 0, 0)');
+        expect(style['font-size']).toBe('14px');
         expect(style).toHaveProperty('color');
         expect(style).toHaveProperty('font-size');
         expect(style).toHaveProperty('padding-top');
         expect(style).toHaveProperty('display');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/extension/utils/dom/style-extract.test.ts` around lines 13 - 20, The
test for extractStyle() only verifies that style keys exist and that the total
key count matches, so it can still pass even if the returned values are empty.
Tighten the assertions in style-extract.test.ts by checking the computed values
for the properties seeded in the setup, especially color and font-size,
alongside the existing property presence checks. Use extractStyle() and the
style object in the test to assert the expected concrete values rather than only
key existence.
apps/extension/utils/linear/index.ts (1)

8-23: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider adding a request timeout to the GraphQL fetch.

gql has no AbortController/timeout, so a stalled connection leaves verifyKey/listTeams/createIssue pending indefinitely, with no recovery path for the popup/sidepanel UIs that await them.

♻️ Proposed timeout via AbortSignal
 const gql = async <T>(key: string, query: string, variables?: object): Promise<T> => {
     const res = await fetch(ENDPOINT, {
         method: 'POST',
         // Linear Personal API Key는 Authorization 헤더에 값 그대로. Bearer 접두사 없음.
         headers: { 'Content-Type': 'application/json', Authorization: key },
         body: JSON.stringify({ query, variables }),
+        signal: AbortSignal.timeout(15_000),
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/extension/utils/linear/index.ts` around lines 8 - 23, The gql helper
currently has no timeout or abort handling, so requests used by verifyKey,
listTeams, and createIssue can hang forever. Update gql to use an
AbortController-based timeout for the fetch call, and make sure
timeout-triggered aborts surface as a clear error path that the callers can
handle. Keep the change localized to the gql function so all Linear GraphQL
requests inherit the same recovery behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/extension/entrypoints/fiber-reader.ts`:
- Line 16: Restrict the `window.postMessage` call in `fiber-reader` so it no
longer uses the wildcard target origin; update the `FIBER_RESPONSE` response
path to send to `window.location.origin` instead of `'*'`. Keep the existing
`requestId` and `components` payload unchanged, and make sure the same-window
request/response flow still works while satisfying origin validation.

In `@apps/extension/entrypoints/inspector.content/index.ts`:
- Around line 94-97: The capture sequence in the inspector content flow still
risks grabbing the overlay/FAB because a single nextFrame call may happen before
the hide changes are painted. Update the capture path in index.ts around
overlay?.setVisible(false), uiHost.style.display, and the await nextFrame() wait
so it uses an after-paint delay such as a double rAF before captureVisibleTab is
called.

In `@apps/extension/entrypoints/inspector.content/inspector-ui.tsx`:
- Around line 33-45: The MemoBubble in inspector-ui.tsx is being reused when pin
changes, so partially typed memo state can leak across targets. Update the
MemoBubble render so it remounts whenever the pinned target changes by giving it
a stable key derived from the current pin target identity (not just the
component presence), and keep the existing onSave/onCancel behavior in the
Inspector UI path.

In `@apps/extension/entrypoints/inspector.content/lightbox.ts`:
- Around line 13-15: The lightbox replacement path in showLightbox removes the
existing DOM node directly, which skips the cleanup done by close() and leaves
resize/keydown listeners behind. Update showLightbox to tear down any existing
lightbox by invoking the same close/cleanup flow used by close() before creating
the new lightbox, so reopening the inspector lightbox does not accumulate
handlers.

In `@apps/extension/entrypoints/inspector.content/memo-bubble.test.tsx`:
- Around line 12-17: The test in memo-bubble.test.tsx mutates the global
window.innerHeight and leaves it changed for later tests. Update the existing
afterEach alongside document.body.replaceChildren() to restore
window.innerHeight after the memo bubble viewport test, using the same test
setup around innerHeight so the global browser state does not leak between
happy-dom tests.

In `@apps/extension/entrypoints/popup/useApiKey.ts`:
- Around line 28-50: The key-loading flow in useApiKey() only handles the
successful keyStore.getValue() path, so a rejection leaves status stuck at
loading and the popup spinner never exits. Add error handling to the getValue()
promise chain in useApiKey() so any rejection falls back to setStatus('needKey')
(and optionally clears any stale key state) while still respecting the active
guard, keeping App.tsx from rendering the spinner indefinitely.

In `@apps/extension/entrypoints/sidepanel/ItemCard.tsx`:
- Around line 12-28: The ItemCard preview effect is leaving stale object URLs in
state when imageRef changes or when getImage returns no blob. Update the
useEffect in ItemCard to clear the current url before starting a new lookup, so
the old screenshot is removed immediately when the reference changes. Keep the
existing object URL cleanup in the effect teardown and make sure the reset
happens even when imageRef is empty or the fetch result is missing.

In `@apps/extension/entrypoints/sidepanel/RegisterBar.tsx`:
- Around line 27-48: The RegisterBar useEffect that calls listTeams(apiKey) is
preserving the previous teams and teamId while a new API key is loading, which
can leave a stale team selected. In the RegisterBar component, reset the current
team-related state as soon as apiKey changes by clearing teams and teamId before
or when starting the listTeams request, then repopulate them from the latest
response so submit uses only the current credential’s team selection.

In `@apps/extension/utils/data/session-store.ts`:
- Around line 33-41: The addItem helper is doing a non-atomic read-modify-write
against itemsStore, which can drop entries when multiple extension contexts save
at the same time. Update addItem to use an atomic write path or serialize all
mutations through a single owner so concurrent appends cannot overwrite each
other. Keep the fix localized to addItem and the itemsStore access pattern in
session-store.ts.

In `@apps/extension/utils/linear/image-sharing.ts`:
- Around line 15-28: The viewport lookup in the image-sharing helper is too
permissive when multiple existing items share the same `tabId`, `pageUrl`,
`scrollX`, `scrollY`, and `width` but have different `imageRef`s. Update the
logic in the function that builds `sameViewport` so it detects more than one
distinct `imageRef` and returns `null` instead of picking the first ref and
mixing `nextIndex` values; keep sharing only when all matching items point to
the same screenshot reference.

---

Nitpick comments:
In `@apps/extension/package.json`:
- Line 8: The package.json scripts include a redundant compile alias that runs
the same tsc --noEmit command as typecheck, so remove the compile entry and keep
typecheck as the single canonical script name. Update the scripts block in
package.json to avoid duplicate aliases and ensure any references align with
typecheck.

In `@apps/extension/utils/data/image-store.test.ts`:
- Around line 11-24: The current tests only verify conversion helpers, so add
one automated IndexedDB round-trip test that exercises the real persistence flow
through putImage, getImage, and clearImages in image-store.test.ts. Use the
existing image-store APIs to store a Blob or data URL, retrieve it back, assert
the persisted payload matches, and then clear it to confirm cleanup works. Keep
the new test aligned with the existing helper-focused style, but make it cover
the actual IDB-backed path end to end.

In `@apps/extension/utils/dom/style-extract.test.ts`:
- Around line 13-20: The test for extractStyle() only verifies that style keys
exist and that the total key count matches, so it can still pass even if the
returned values are empty. Tighten the assertions in style-extract.test.ts by
checking the computed values for the properties seeded in the setup, especially
color and font-size, alongside the existing property presence checks. Use
extractStyle() and the style object in the test to assert the expected concrete
values rather than only key existence.

In `@apps/extension/utils/linear/build-issue.test.ts`:
- Around line 92-106: The `buildDescription()` test covers the no-upload
data-URL path but does not verify the scaling input passed into `annotateImage`.
Update the test around `buildDescription`, `mocks.annotateImage`, and the
`item({ imageRef, width })` setup to assert that the `head.width` value is
forwarded as the width argument when `annotateImage` is called. Keep the
existing no-upload assertions and add a check on the `annotateImage` mock call
so this contract cannot regress silently.

In `@apps/extension/utils/linear/index.ts`:
- Around line 8-23: The gql helper currently has no timeout or abort handling,
so requests used by verifyKey, listTeams, and createIssue can hang forever.
Update gql to use an AbortController-based timeout for the fetch call, and make
sure timeout-triggered aborts surface as a clear error path that the callers can
handle. Keep the change localized to the gql function so all Linear GraphQL
requests inherit the same recovery behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a9e53a8c-479c-4566-a89a-2271a6f6d324

📥 Commits

Reviewing files that changed from the base of the PR and between 3a87c86 and 38c3613.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml, !pnpm-lock.yaml
📒 Files selected for processing (56)
  • .prettierignore
  • apps/extension/.gitignore
  • apps/extension/CLAUDE.md
  • apps/extension/entrypoints/background.ts
  • apps/extension/entrypoints/fiber-reader.ts
  • apps/extension/entrypoints/inspector.content/fab.tsx
  • apps/extension/entrypoints/inspector.content/index.ts
  • apps/extension/entrypoints/inspector.content/inspector-ui.tsx
  • apps/extension/entrypoints/inspector.content/lightbox.ts
  • apps/extension/entrypoints/inspector.content/memo-bubble.test.tsx
  • apps/extension/entrypoints/inspector.content/memo-bubble.tsx
  • apps/extension/entrypoints/inspector.content/overlay.test.ts
  • apps/extension/entrypoints/inspector.content/overlay.ts
  • apps/extension/entrypoints/popup/ApiKeyForm.tsx
  • apps/extension/entrypoints/popup/App.tsx
  • apps/extension/entrypoints/popup/index.html
  • apps/extension/entrypoints/popup/main.tsx
  • apps/extension/entrypoints/popup/useApiKey.ts
  • apps/extension/entrypoints/popup/useInspecting.ts
  • apps/extension/entrypoints/popup/useQaSession.ts
  • apps/extension/entrypoints/sidepanel/App.tsx
  • apps/extension/entrypoints/sidepanel/ItemCard.tsx
  • apps/extension/entrypoints/sidepanel/RegisterBar.test.tsx
  • apps/extension/entrypoints/sidepanel/RegisterBar.tsx
  • apps/extension/entrypoints/sidepanel/index.html
  • apps/extension/entrypoints/sidepanel/main.tsx
  • apps/extension/entrypoints/sidepanel/useQaItems.ts
  • apps/extension/entrypoints/sidepanel/useStoredApiKey.ts
  • apps/extension/eslint.config.mjs
  • apps/extension/package.json
  • apps/extension/tsconfig.json
  • apps/extension/utils/browser/active-tab.ts
  • apps/extension/utils/browser/background.test.ts
  • apps/extension/utils/data/image-store.test.ts
  • apps/extension/utils/data/image-store.ts
  • apps/extension/utils/data/session-store.ts
  • apps/extension/utils/dom/annotate-image.test.ts
  • apps/extension/utils/dom/annotate-image.ts
  • apps/extension/utils/dom/fiber-component.test.ts
  • apps/extension/utils/dom/fiber-component.ts
  • apps/extension/utils/dom/selector.ts
  • apps/extension/utils/dom/style-extract.test.ts
  • apps/extension/utils/dom/style-extract.ts
  • apps/extension/utils/linear/build-issue.test.ts
  • apps/extension/utils/linear/build-issue.ts
  • apps/extension/utils/linear/group-by-image.test.ts
  • apps/extension/utils/linear/group-by-image.ts
  • apps/extension/utils/linear/image-sharing.test.ts
  • apps/extension/utils/linear/image-sharing.ts
  • apps/extension/utils/linear/index.ts
  • apps/extension/utils/messaging/fiber-protocol.test.ts
  • apps/extension/utils/messaging/fiber-protocol.ts
  • apps/extension/utils/messaging/index.ts
  • apps/extension/vitest.config.ts
  • apps/extension/wxt.config.ts
  • turbo.json

Comment thread apps/extension/entrypoints/fiber-reader.ts Outdated
Comment thread apps/extension/entrypoints/inspector.content/index.ts
Comment thread apps/extension/entrypoints/inspector.content/inspector-ui.tsx
Comment thread apps/extension/entrypoints/inspector.content/lightbox.ts
Comment thread apps/extension/entrypoints/inspector.content/memo-bubble.test.tsx
Comment thread apps/extension/entrypoints/popup/useApiKey.ts Outdated
Comment thread apps/extension/entrypoints/sidepanel/ItemCard.tsx
Comment thread apps/extension/entrypoints/sidepanel/RegisterBar.tsx
Comment on lines +33 to +41
export const addItem = async (item: Omit<QaItem, 'id' | 'createdAt'>): Promise<QaItem> => {
const entry: QaItem = {
...item,
id: crypto.randomUUID(),
createdAt: Date.now(),
};

const current = await itemsStore.getValue();
await itemsStore.setValue([...current, entry]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make appends atomic across contexts.

addItem() does a read-modify-write on local:qaItems. If two saves overlap, both can read the same array and the later setValue() drops the earlier entry. In this PR the store is shared across multiple extension contexts, so this can surface as missing QA items under concurrent saves. Serialize writes through one owner or switch this helper to an atomic update path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/extension/utils/data/session-store.ts` around lines 33 - 41, The addItem
helper is doing a non-atomic read-modify-write against itemsStore, which can
drop entries when multiple extension contexts save at the same time. Update
addItem to use an atomic write path or serialize all mutations through a single
owner so concurrent appends cannot overwrite each other. Keep the fix localized
to addItem and the itemsStore access pattern in session-store.ts.

Comment on lines +15 to +28
const sameViewport = items.filter(
(item) =>
item.tabId === tabId &&
item.pageUrl === pageUrl &&
item.scrollX === scrollX &&
item.scrollY === scrollY &&
item.width === width &&
item.imageRef != null,
);
if (sameViewport.length === 0) return null;

const imageRef = sameViewport[0].imageRef!;
const nextIndex = Math.max(...sameViewport.map((item) => item.index ?? 0)) + 1;
return { imageRef, nextIndex };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Refuse ambiguous viewport matches.

If sameViewport already contains two distinct imageRefs, this returns the first ref but computes nextIndex across both groups. The next item then gets attached to an arbitrary screenshot with numbering taken from another one. Detect multiple refs and return null so the caller captures a fresh image instead.

Possible fix
     if (sameViewport.length === 0) return null;

-    const imageRef = sameViewport[0].imageRef!;
+    const imageRefs = new Set(sameViewport.map((item) => item.imageRef));
+    if (imageRefs.size !== 1) return null;
+
+    const [imageRef] = [...imageRefs] as string[];
     const nextIndex = Math.max(...sameViewport.map((item) => item.index ?? 0)) + 1;
     return { imageRef, nextIndex };
As per coding guidelines, "Only share an image reference when `tabId`, `pageUrl`, `scrollX`, `scrollY`, and `width` all match; otherwise create a new capture."
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const sameViewport = items.filter(
(item) =>
item.tabId === tabId &&
item.pageUrl === pageUrl &&
item.scrollX === scrollX &&
item.scrollY === scrollY &&
item.width === width &&
item.imageRef != null,
);
if (sameViewport.length === 0) return null;
const imageRef = sameViewport[0].imageRef!;
const nextIndex = Math.max(...sameViewport.map((item) => item.index ?? 0)) + 1;
return { imageRef, nextIndex };
const sameViewport = items.filter(
(item) =>
item.tabId === tabId &&
item.pageUrl === pageUrl &&
item.scrollX === scrollX &&
item.scrollY === scrollY &&
item.width === width &&
item.imageRef != null,
);
if (sameViewport.length === 0) return null;
const imageRefs = new Set(sameViewport.map((item) => item.imageRef));
if (imageRefs.size !== 1) return null;
const [imageRef] = [...imageRefs] as string[];
const nextIndex = Math.max(...sameViewport.map((item) => item.index ?? 0)) + 1;
return { imageRef, nextIndex };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/extension/utils/linear/image-sharing.ts` around lines 15 - 28, The
viewport lookup in the image-sharing helper is too permissive when multiple
existing items share the same `tabId`, `pageUrl`, `scrollX`, `scrollY`, and
`width` but have different `imageRef`s. Update the logic in the function that
builds `sameViewport` so it detects more than one distinct `imageRef` and
returns `null` instead of picking the first ref and mixing `nextIndex` values;
keep sharing only when all matching items point to the same screenshot
reference.

Source: Coding guidelines

@noahchoii noahchoii left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 AI를 이용해서 코드를 작성하면서 많이 체감했던 부분인데, 아무래도 인간보다 엣지 케이스에 대해 더 고려할 수 있는 부분이 많기 때문에 그런지 if 분기문이 점점 많아지는 것 같아요. 또 삼항연산자도 점점 많아지고 삼항연산자를 중첩으로 사용하는 경우도 꽤나 자주 발생했던 것 같고요. 그래서 코드를 읽을 때 인지 부하가 더욱 커지는 것 같습니다..!

그래서 우리도 앞으로 이런 식의 제품을 제작할 일이 많아질텐데, 이전에 Gemini codereview를 이용했던 것처럼 공용으로 사용할 컨벤션 스킬을 만들어둘 필요가 있겠다는 생각이 들었습니다!

--

폴더명이 extension이라고만 되어 있는데, qa-extensions처럼 이름을 확실히 구분할 수 있도록 수정하는 것은 어떨까요?

--

코드양이 많기도 하고, 어차피 우리가 사용할 도구라서 일단 추가 후 도그푸딩할 겸 우선 승인해두겠습니다!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants