Skip to content

feat: auto display labels, billing, login UX, and popover scroll fixes#394

Merged
bensonwong merged 15 commits intomainfrom
feat/auto-display-labels
Mar 31, 2026
Merged

feat: auto display labels, billing, login UX, and popover scroll fixes#394
bensonwong merged 15 commits intomainfrom
feat/auto-display-labels

Conversation

@bensonwong
Copy link
Copy Markdown
Collaborator

@bensonwong bensonwong commented Mar 31, 2026

Summary

  • Auto display labels: When visible citation text differs from anchorText, automatically set displayLabel so the popover shows what the user actually sees (582250f)
  • Billing & usage warnings: Add PaymentRequiredError, usage-warning checks, billing CLI command, extract BILLING_URL constant (0cc5d2a, 04dadda, 02def0b, ad060ae)
  • Login UX: Terminal key-paste fallback + cancel for login command; print free-tier welcome on browser OAuth (65498c4, 3c798ee)
  • OCR hull-merge: Merge fragmented anchor-text bounding rects for OCR-split items so highlight overlays cover the full phrase (c2531b0)
  • Popover scroll stability: Portal popover into the trigger's scroll-root ancestor (Radix ScrollArea viewport or nearest overflow:scroll/auto parent) using position:absolute instead of position:fixed. The popover now scrolls with page content, fixing scroll-eating in apps with body { overflow: hidden }. Applied to both React and vanilla CDN runtimes (5adacdd, 4953a1e, a91a344, 72267ef, 8a76476)
  • Focus-trap fix: Rewrite inert focus-trap to walk from popover up to <body>, inerting siblings at each level — handles the popover now being inside <main> via the scroll container portal

Test plan

  • Verify popover scrolls with page content (not fixed to viewport)
  • Verify popover positions correctly on open in a Radix ScrollArea consumer
  • Verify keyboard focus trap works — Tab stays within popover when opened via keyboard
  • Verify billing CLI command opens billing URL
  • Verify login command with terminal key-paste fallback
  • Verify OCR-fragmented citation highlights cover the full anchor text
  • Verify display labels auto-set when visible text differs from anchorText
  • Run bun run test — 1713 tests pass
  • Run bun run lint — no errors (warnings pre-existing)

Benson and others added 13 commits March 31, 2026 11:28
- Add PaymentRequiredError (402) with billingCode field; previously 402s
  fell through to the generic ValidationError catch-all in createApiError
- Add checkUsageWarning() in DeepCitation client that reads X-DeepCitation-
  Remaining / X-DeepCitation-Limit response headers and fires onUsageWarning
  callback — called alongside checkLatestVersion on every successful response
- Add onUsageWarning callback to DeepCitationConfig (types.ts)
- Add USAGE_WARN_PCT=80 and USAGE_CRITICAL_PCT=90 to new billing.ts as the
  canonical threshold source; export from index.ts
- CLI: formatNetworkError now detects PaymentRequiredError and emits an
  actionable message with billing URL and pay-as-you-go benefits
- CLI: warnUsage() fires when budget hits USAGE_WARN_PCT / USAGE_CRITICAL_PCT
- CLI: new `billing` command opens ${BASE_URL}/api#billing in the browser

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- checkUsageWarning: add `!response.ok` early-return so the callback
  never fires on 402/5xx responses (contract states successful responses only)
- formatNetworkError: wrap err.message through sanitizeForLog() before
  printing — the message comes from untrusted server JSON and could carry
  ANSI sequences or log-injection payloads
- deepCitationPricing.ts: remove re-export of USAGE_WARN_PCT / USAGE_CRITICAL_PCT
  (violates no-variable-re-export policy); ApiBillingPage now imports
  USAGE_WARN_PCT directly from deepcitation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…op hardcode

- Extract ${BASE_URL}/api#billing to BILLING_URL const (was duplicated in
  warnUsage, formatNetworkError, openBillingDashboard)
- billing case now uses formatNetworkError() like every other command handler
  (was inlining err instanceof Error ternary)
- Remove hardcoded "$20/month" from openBillingDashboard copy — free tier
  amount is not this package's responsibility to hardcode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extends the first-connection messaging to the browser OAuth path — both
the API-key and browser login flows now call printFreeTierWelcome() so
every new user sees the $20/month free tier notice and billing URL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of warning about display-label mismatches, the inject command now
automatically adds data-dc-display-label to elements where the visible
text doesn't match the anchorText. This lets the skill skip manual
display-label audits — the CLI handles it post-hoc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- `startCallbackServer` now returns a `cancel()` function that closes
  the HTTP server and rejects the pending result promise.
- `readKeyFromStdin()` races against the browser callback: if the user
  pastes a valid `sk-dc-...` key into the terminal while the browser
  flow is pending, the server is cancelled and the key is saved
  directly via `saveApiKey`.
- Putting stdin into readline flowing mode also prevents typed
  characters from leaking into the shell after process exit.
- Fix self-referential `const BILLING_URL = BILLING_URL` typo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the gap-fill approach (patch gaps < MAX_GAP_PCT between adjacent
word rects) with a hull-merge: all rects on the same line are collapsed
into a single spanning rect whose left/right are the outermost edges.

The gap-fill had a hard 3% threshold that silently missed larger gaps
produced by character-level OCR fragmentation (e.g. "ss", "sso", "ee"
returned as separate bounding boxes for a single word). Hull-merge
absorbs any gap size, so the highlight always covers the full text run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
`getBoundingClientRect()` returns viewport-relative coordinates.
The previous code added `window.scrollX/Y` to convert to document
space, which was correct for `position:absolute` but double-counts
scroll for `position:fixed`.

Switching to `position:fixed` (both React and CDN paths) lets the
popover stay anchored to the viewport without any scroll arithmetic.
`computePosition` in positioning.ts now returns raw viewport coords.

Also: forward `data-dc-display-label` attribute into `CdnPopoverWrapper`
so the CDN runtime honours auto-display-labels set by the attributor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…size

Chrome fires ResizeObserver when a scrolled element's visible rect
changes, causing the popover to reposition and follow the trigger during
scroll. Remove trigger observation from both React and vanilla runtimes;
window "resize" listener already covers legitimate trigger-resize cases.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ute positioning

position:fixed breaks scroll passthrough in apps using overflow:hidden on
body with a Radix ScrollArea as the actual scroll container. Portal the
popover wrapper into the trigger's scroll-root ancestor and use
position:absolute with container-relative coords so it scrolls naturally
with page content. Applied to both React (Popover.tsx) and vanilla CDN
runtime (cdn.ts).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix focus-trap inert: walk from popover up to body inerting siblings
  at each level, instead of inerting <main> (which now contains the
  popover portal)
- Add SSR guard in portal detection useLayoutEffect
- Cleanup radixVP.style.position mutation on effect teardown
- Re-detect scroll root on each open (add `open` to deps)
- Add scrollHeight > clientHeight guard to fallback scroll detection
- Guard against detached scroll containers in CDN runtime
- Fix misleading comment about initial wrapper attachment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Matches the React path which cleans up via useLayoutEffect teardown.
Without this, the CDN runtime permanently sets position:relative on
the Radix ScrollArea viewport.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown

vercel bot commented Mar 31, 2026

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

4 Skipped Deployments
Project Deployment Actions Updated (UTC)
agui-chat-deepcitation Ignored Ignored Preview Mar 31, 2026 11:43am
deepcitation-langchain-rag-chat Ignored Ignored Preview Mar 31, 2026 11:43am
mastra-rag-deepcitation Ignored Ignored Preview Mar 31, 2026 11:43am
nextjs-ai-sdk-deepcitation Ignored Ignored Preview Mar 31, 2026 11:43am

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 31, 2026

✅ Playwright Test Report

Status: Tests passed

📊 Download Report & Snapshots (see Artifacts section)

What's in the Visual Snapshots

The gallery includes visual snapshots for:

  • 🖥️ Desktop showcase (all variants × all states)
  • 📱 Mobile showcase (iPhone SE viewport)
  • 📟 Tablet showcase (iPad viewport)
  • 🔍 Popover states (verified, partial, not found)
  • 🔗 URL citation variants

Run ID: 23795512476

inerted.push(child);
// Walk from popover up to body, inerting siblings at each level.
let current: Element | null = popoverEl;
while (current && current !== document.body) {
Benson and others added 2 commits March 31, 2026 18:36
- Fix TS7022 in Citation.tsx: rename `parent` to `parentEl` with
  explicit Element type to break circular inference
- Cast parent.children to Element[] for proper typing
- Remove extra `triggerRef` from useEffect dep arrays (stable ref)
- Auto-format fuzzyAnchor.test.ts and cli.ts for biome

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…eWarning

- Fix CodeQL high: loop HTML tag stripping until stable to handle
  nested fragments like <scr<script>ipt>
- Update popoverScrollStability Playwright test: verify popover scrolls
  with page content (position:absolute) instead of staying viewport-fixed
- Rename onUsageWarning → onUsageUpdate: the callback fires on every
  response with usage headers, not just near limits

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bensonwong bensonwong merged commit aa97aff into main Mar 31, 2026
13 checks passed
@bensonwong bensonwong deleted the feat/auto-display-labels branch March 31, 2026 11:48
@claude
Copy link
Copy Markdown

claude bot commented Mar 31, 2026

PR Review: feat: auto display labels, billing, login UX, and popover scroll fixes

This is a substantial PR bundling five feature areas. The OCR hull-merge, fuzzy anchor tests, and focus-trap generalization are solid. Below is a prioritized list of issues to address before merge.


Bugs

[HIGH] Double printFreeTierWelcome() on terminal-paste login path

src/cli.tssaveApiKey() already calls printFreeTierWelcome() internally (~line 956). The login function then calls it again unconditionally after the if/else block (~line 1091). Users who paste a key in the terminal will see the welcome message printed twice.

Fix: Call printFreeTierWelcome() only in the winner.from === "browser" branch, or remove the call inside saveApiKey when invoked from login.

[MEDIUM] Stale position:fixed comment in positioning.ts

src/vanilla/runtime/positioning.ts lines 30-31 still say the wrapper uses position:fixed. The wrapper is now position:absolute inside a scroll container (as set in cdn.ts). The math is correct, but the comment is actively misleading. Please update it.

[MEDIUM] Fragile string comparison for cancel detection

src/cli.ts ~line 1093:

if ((err as Error).message === "Login cancelled") return;

This couples login() to the exact string in auth.ts's cancel(). If that message ever changes, the catch silently stops working. Use a dedicated CancelledError class or a sentinel Symbol.

[LOW] Nested-element regex can match inner closing tag

src/cli.ts ~line 603 uses [\s\S]*? non-greedy across element content. For an annotated <span> containing a nested <span>, it captures the inner close tag, producing a truncated visibleText and wrong displayLabel. The 80-char length guard reduces impact but does not eliminate the bug.


Security

[MEDIUM] Incomplete HTML attribute escaping in auto display label injection

src/cli.ts ~line 615:

const escaped = visibleText.replace(/"/g, "&quot;");

Only " is escaped. Single quotes, <, and > are not. Use a proper HTML attribute escaper covering &, <, >, ", and ' — consistent with the safeReplace() pattern used elsewhere.

@claude
Copy link
Copy Markdown

claude bot commented Mar 31, 2026


Code Quality

[LOW] BILLING_URL constant bypassed in warnUsage()

warnUsage() hardcodes ${BASE_URL}/api#billing directly at two call sites (~lines 277, 291) instead of using the BILLING_URL constant. Use BILLING_URL consistently.

[LOW] Unnecessary local alias in openBillingDashboard

const url = BILLING_URL;  // redundant — use BILLING_URL directly

[LOW] Inline as-cast on raw API response data

The inject logic casts verifications (raw API JSON) via an inline as { citation?: { anchorText?: string } }. Use a type guard or a properly-typed verifications type instead.

@claude
Copy link
Copy Markdown

claude bot commented Mar 31, 2026


Test Coverage

[MEDIUM] PaymentRequiredError has no unit test

src/__tests__/client/errors.test.ts covers AuthenticationError, NetworkError, RateLimitError, and ValidationError but not PaymentRequiredError. Add cases verifying billingCode, retryable, and httpStatus.

[MEDIUM] createApiError 402-branch has no test

The existing client.test.ts suite covers 401, 429, 422, and 500 responses but not 402.

[MEDIUM] onUsageWarning callback has no test

No test for: callback fires with correct used/limit values; callback does NOT fire when headers are absent; NaN-guarded case when header value is malformed.

[LOW] Auto display label injection not tested

No tests in cliInject.test.ts for the new auto-fix path: happy path, skip when label already present, length-guard (>80 chars), skip when visible text is already a substring of anchorText.

@claude
Copy link
Copy Markdown

claude bot commented Mar 31, 2026


Stale Test Comment

tests/playwright/specs/popoverScrollStability.spec.tsx lines 1-12 describe the old position:fixed behavior. The test body validates the new scroll-with-container behavior. Please update the comment.


Informational

USAGE_WARN_PCT and USAGE_CRITICAL_PCT are exported from src/index.ts, making them public API. If these are internal CLI thresholds, remove from the public export. If intentional, add JSDoc clarifying SDK consumer use cases.


Positives

  • OCR hull-merge cleanly replaces gap-fill and correctly handles character-level fragmentation.
  • fuzzyAnchor.test.ts is thorough: 60% threshold, OCR word-split artifacts, edge cases, known limitation pinned.
  • The inert focus-trap rewrite (walking from popover to <body>) correctly handles portalling inside <main>.
  • cancel() in startCallbackServer is a clean API — promise rejected, server and timeout both cleaned up.
  • checkUsageWarning correctly gates on response.ok and uses Number.isNaN for header-absence detection.

Summary: The double-welcome-message bug (HIGH), incomplete HTML attribute escaping (MEDIUM/Security), and three missing billing/usage tests (MEDIUM) should be resolved before merge. The other items are lower-risk but worth addressing while the code is fresh.

@claude
Copy link
Copy Markdown

claude bot commented Mar 31, 2026

PR Review: feat: auto display labels, billing, login UX, and popover scroll fixes

This is a substantial PR bundling five feature areas. The OCR hull-merge, fuzzy anchor tests, and focus-trap generalization are solid. Below is a prioritized list of issues to address before merge.


Bugs

[HIGH] Double printFreeTierWelcome() on terminal-paste login path

src/cli.tssaveApiKey() already calls printFreeTierWelcome() internally (~line 956). The login function then calls it again unconditionally after the if/else block (~line 1091). Users who paste a key in the terminal will see the welcome message printed twice.

Fix: Call printFreeTierWelcome() only in the winner.from === "browser" branch, or remove the call inside saveApiKey when invoked from login.

[MEDIUM] Stale position:fixed comment in positioning.ts

src/vanilla/runtime/positioning.ts lines 30-31 still say the wrapper uses position:fixed. The wrapper is now position:absolute inside a scroll container (as set in cdn.ts). The math is correct, but the comment is actively misleading. Please update it.

[MEDIUM] Fragile string comparison for cancel detection

src/cli.ts ~line 1093:

if ((err as Error).message === "Login cancelled") return;

This couples login() to the exact string in auth.ts's cancel(). If that message ever changes, the catch silently stops working. Use a dedicated CancelledError class or a sentinel Symbol.

[LOW] Nested-element regex can match inner closing tag

src/cli.ts ~line 603 uses [\s\S]*? non-greedy across element content. For an annotated <span> containing a nested <span>, it captures the inner close tag, producing a truncated visibleText and wrong displayLabel. The 80-char length guard reduces impact but does not eliminate the bug.


Security

[MEDIUM] Incomplete HTML attribute escaping in auto display label injection

src/cli.ts ~line 615:

const escaped = visibleText.replace(/"/g, "&quot;");

Only " is escaped. Single quotes, <, and > are not. Use a proper HTML attribute escaper covering &, <, >, ", and ' — consistent with the safeReplace() pattern used elsewhere.


Code Quality

[LOW] BILLING_URL constant bypassed in warnUsage()

warnUsage() hardcodes ${BASE_URL}/api#billing directly at two call sites (~lines 277, 291) instead of using the BILLING_URL constant. Use BILLING_URL consistently.

[LOW] Unnecessary local alias in openBillingDashboard

const url = BILLING_URL;  // redundant — use BILLING_URL directly

[LOW] Inline as-cast on raw API response data

The inject logic casts verifications (raw API JSON) via an inline as { citation?: { anchorText?: string } }. Use a type guard or a properly-typed verifications type instead.


Test Coverage

[MEDIUM] PaymentRequiredError has no unit test

src/__tests__/client/errors.test.ts covers AuthenticationError, NetworkError, RateLimitError, and ValidationError but not PaymentRequiredError. Add cases verifying billingCode, retryable, and httpStatus.

[MEDIUM] createApiError 402-branch has no test

The existing client.test.ts suite covers 401, 429, 422, and 500 responses but not 402.

[MEDIUM] onUsageWarning callback has no test

No test for: callback fires with correct used/limit values; callback does NOT fire when headers are absent; NaN-guarded case when header value is malformed.

[LOW] Auto display label injection not tested

No tests in cliInject.test.ts for the new auto-fix path: happy path, skip when label already present, length-guard (>80 chars), skip when visible text is already a substring of anchorText.


Stale Test Comment

tests/playwright/specs/popoverScrollStability.spec.tsx lines 1-12 describe the old position:fixed behavior. The test body validates the new scroll-with-container behavior. Please update the comment.


Informational

USAGE_WARN_PCT and USAGE_CRITICAL_PCT are exported from src/index.ts, making them public API. If these are internal CLI thresholds, remove from the public export. If intentional, add JSDoc clarifying SDK consumer use cases.


Positives

  • OCR hull-merge cleanly replaces gap-fill and correctly handles character-level fragmentation.
  • fuzzyAnchor.test.ts is thorough: 60% threshold, OCR word-split artifacts, edge cases, known limitation pinned.
  • The inert focus-trap rewrite (walking from popover to <body>) correctly handles portalling inside <main>.
  • cancel() in startCallbackServer is a clean API — promise rejected, server and timeout both cleaned up.
  • checkUsageWarning correctly gates on response.ok and uses Number.isNaN for header-absence detection.

Summary: The double-welcome-message bug (HIGH), incomplete HTML attribute escaping (MEDIUM/Security), and three missing billing/usage tests (MEDIUM) should be resolved before merge. The other items are lower-risk but worth addressing while the code is fresh.

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