Four items from gemini and claude reviews:
- botDetect.ts: Memoize isBot() result. navigator.userAgent is immutable
for the page lifetime, so re-running the isbot regex on every Turnstile
pre-mint debounce and post-submit re-mint check is wasted work. SSR
fallback is intentionally not cached so a module instance reused across
SSR/CSR still reaches the real UA check on first CSR call.
- usePackRequest.ts: Disambiguate the "submit-path NOT gated" comment —
it was confusing because the new post-submit re-mint also lives inside
submitRequest's finally. Reworded to "click-path acquireTurnstileToken"
to make clear which call site is intentionally skipped.
- usePackRequest.ts: Update the userTouched comment to reflect autofill
reality — modern Chromium/Firefox DO fire input events on autofill, so
the rationale ("autofill doesn't trigger") was already stale. The new
isBot() guard covers the gap for well-behaved crawler UAs.
- usePackRequest.ts: Add English glosses for the Japanese CF dashboard
labels (提示チャレンジ / 未解決) so non-Japanese-reading maintainers can
follow the comment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CF Turnstile dashboard shows ~17k unsolved challenges over the past
7 days vs ~10k solved — a 2:1 unsolved:solved ratio that's larger than
the post-submit auto re-mint can explain. Most of those are JS-executing
crawlers (Slackbot, Discord card validator, Twitter card validator, X,
Apple link preview, Googlebot, etc.) that render the page, somehow trigger
a DOM input event on the URL field (autofill / accessibility tools /
focus tricks), and pay a CF challenge they can't solve.
Add an `isBot()` short-circuit at the two pre-mint call sites in
`usePackRequest`:
- the debounced `onTrigger` after `markUserTouched` flips
- the post-submit auto re-mint in `submitRequest`'s finally block
The actual security gate is the server-side `siteverify` in
`turnstileMiddleware` — that stays the only authoritative check, so a
crawler that spoofs UA past `isBot()` still gets blocked there. The
submit-path `takeToken()` is intentionally NOT gated to avoid
false-positive lockouts of legitimate users with unusual UAs (e.g. older
clients, accessibility tools).
Net effect:
- "提示チャレンジ" / "未解決" CF dashboard counters drop sharply
(well-behaved crawlers stop minting unsolvable tokens)
- `pack_completed` server logs unaffected (legit users don't change
paths; bots couldn't reach `/api/pack` either way)
- server-side spend on siteverify unchanged
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six items from claude's incremental review (`12:48:43Z`):
- monitoring/dashboard.json: Group the outcomes widget by both
`metric.label.outcome` and `metric.label.reason`. Previously all
failures collapsed into a single `turnstile_failed` series, which
contradicted the README claim that the `reason` label drives the
breakdown.
- monitoring/metrics/*.yaml: Narrow the metric filter to
`jsonPayload.event=("turnstile_siteverify" OR "pack_completed")`.
Without this anchor, any future code path attaching
`siteverifyDurationMs` to an unrelated log silently joins the
distribution and creates new metric label values.
- usePackRequest.ts: Mirror `progressMessage.value = null` alongside
the `progressStage.value = null` clear on token-acquisition aborted /
error branches. Prevents a future edit setting a verifying message
from leaking prior-run state.
- turnstile.test.ts: Add a focused `describe` block with five tests
asserting `siteverifyDurationMs` is attached to every post-siteverify
log (one success path + four reject branches). The metric YAML
filters on field presence, so a refactor that drops the field on any
branch would silently break the metric without other tests failing.
Uses the existing `vi.spyOn(logger, ...)` pattern; no clock injection
needed.
- monitoring/README.md: Note that the metric filter pins
`service_name="repomix-server-us"`, so future regions (`-eu`,
`-asia`) silently drop out until the filter is broadened or
per-region counterparts applied.
- monitoring/README.md: Add a `gcloud logging metrics describe` snippet
for verifying a YAML edit was actually applied (gcloud update is
silent on no-op vs effective change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six items from gemini, claude initial review, and claude follow-up:
- turnstile.ts: Update misleading comment that claimed the metric filters
on `event=turnstile_siteverify` and `outcome=success`. The actual
Cloud Monitoring metrics in `monitoring/metrics/` filter on
`siteverifyDurationMs` field presence, which uniformly captures both
the parallel success log (event=turnstile_siteverify) and the four
rejectAndLog failure paths (event=pack_completed). The comment
contradicted README and YAML and would mislead future readers.
- turnstile.ts: Wrap rejectAndLog in a local `rejectWithDuration` helper
so every post-siteverify branch automatically carries
`siteverifyDurationMs`. Prevents drift if a fifth reject reason gets
added later.
- client.ts: Split the wire-protocol `PackProgressStage` (server-emitted
SSE values) from the display-only `DisplayProgressStage` superset that
adds `verifying`. Keeping the synthetic stage out of the wire type
prevents silent divergence with the server's `PackProgressStage`.
- usePackRequest.ts, TryItLoading.vue, TryItResult.vue: Switch the
display-side type to `DisplayProgressStage`. `onProgress` callbacks
still take the wire `PackProgressStage`.
- usePackRequest.ts: Clear `progressStage` on token-acquisition failure
branches (aborted / error). Functionally invisible since loading=false
hides the loading UI, but prevents the next submit's verifying flash
from briefly showing the previous run's stale state.
- monitoring/metrics/turnstile_siteverify_duration.yaml: Retune the
exponential bucket layout for the 100ms-1s SLO band where decisions
get made. Doubling buckets only placed ~3 boundaries between 100ms
and 1s; growthFactor=1.5 with scale=10 places ~8 boundaries there.
18 finite buckets cover 10ms to ~9.85s, comfortably above the 5s
siteverify timeout so timeouts don't land in overflow.
- monitoring/README.md: Document that pre-network rejections
(secret_missing, missing_token, token_too_long) intentionally don't
carry siteverifyDurationMs, so they're excluded from both metrics
but still appear in the existing pack_requests metric.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes targeting the visible "..." gap between Pack click and the
first SSE progress event observed after PR #1544 landed:
- Client: add a synthetic `verifying` PackProgressStage so the loading
UI displays "Verifying request..." while the server runs Turnstile
siteverify (typically 100-1000ms before the first 'cache-check' SSE
event arrives). The first onProgress callback from handlePackRequest
overwrites it with the real server-reported stage.
- Server: time the siteverify round-trip in `turnstileMiddleware` and
emit `siteverifyDurationMs` on every outcome (success / network
failure / rejected / action mismatch / hostname mismatch). Success
path adds a structured log with `event: turnstile_siteverify` so
Cloud Monitoring can build a log-based distribution metric for
p50/p95/p99 latency and alert on regressions during Cloudflare
incidents.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A valid `?repo=` permalink was treated as an intent signal so the
visitor's click path used a pre-minted token. claude's follow-up review
flagged that this re-creates the dashboard counter inflation this PR is
meant to fix: any third-party page driving traffic to
`https://repomix.com/?repo=<owner/repo>` (Slack / Discord / Twitter card
validators that execute JS) would mint a token per visit, regardless of
whether the visitor ever submits.
Permalink visitors now pay the cold mint on their first click; the user's
first real form interaction (typing, mode click, option tweak, file
upload) is what gates the pre-mint.
Also document the idle-tab cliff in `expired-callback`: re-arming
pre-mint there would burn a challenge every 240s for the lifetime of
an open tab, which is worse than the cold-mint-on-return cost it would
save.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Commit 2a0922d dropped the `error` ref declaration from `useTurnstile`
but left two references behind — `error.value = null` at the top of
`mintToken()` and `error` in the returned object. The first throws
ReferenceError at runtime; the second is a tsc error. No external
caller reads `useTurnstile().error` (TryIt.vue only consumes
`usePackRequest.error`), so the export was already vestigial.
Co-Authored-By: Kazuki Yamada <yamadashy@users.noreply.github.com>
- Drop the unused `error` ref from `useTurnstile`. The widget-level
error-callback writes had no observer (only `usePackRequest.error`
feeds the UI), so the export and writes were vestigial.
- Drop `getResponse` from `TurnstileGlobal`. Never called anywhere in
the codebase; clearer to leave it off the typed surface.
- Don't `console.warn` on normal cancel/timeout flows in
`acquireTurnstileToken`. Move the warn after the `signal.aborted`
check so the dev console only logs genuine challenge / script-load
failures.
- Hoist the consecutive `if (widgetId.value)` guards in `mintToken` by
capturing the rendered widget id into a local const after the throw.
- Drop the redundant `userTouched.value` check in the post-pack
pre-mint guard. `userTouched` is necessarily true at this point —
it was a precondition for `isSubmitValid` being true when the submit
started.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Mark `userTouched=true` when arriving via a valid `?repo=` permalink so
the visitor's click path uses a pre-minted token. Browser autofill and
malformed `?repo=` values still don't burn a challenge.
- After a non-aborted submit completes, schedule a fresh `preMintToken()`
in the finally block. Warms the cache for the typical "view result →
tweak options → repack" flow and for `repackWithSelectedFiles`.
- Reduce the pre-mint debounce from 500ms to 300ms. Tightens the window
where a paste-and-click cadence misses the cache.
- Split composables to fit the 250-line file-size guideline:
* Extract token cache (cache state + single-flight mint + atomic
one-shot consumption) into `useTurnstileTokenCache.ts`. Shrinks
`useTurnstile.ts` from 358 → 241 lines and lets the widget file
focus on render lifecycle / supersede logic.
* Extract pre-mint debounce trigger into `usePreMintDebounce.ts`.
* Extract Turnstile token acquisition + user-facing failure copy into
`turnstileSubmit.ts`. Drops `usePackRequest.ts` from 345 → 331
lines; `submitRequest` is a single cohesive request lifecycle that
resists further splitting.
- Drop the unused `consumed` flag on `CachedToken` (claude review). The
cache nulls the entry on consumption instead, which is what the
takeToken atomic-claim loop already relies on.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop the unused `invalidateCache` export from useTurnstile. Both call
paths (takeToken cache claim, expired-callback) already null
cachedToken inline, so the helper had no callers.
- Update stale `turnstile.getToken()` references in usePackRequest and
useTurnstileScript comments to match the renamed `takeToken()` /
`preMintToken()` API.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- useTurnstile: Make takeToken() one-shot under concurrency. Two callers
awaiting the same shared mintPromise both received the same token,
which siteverify rejects as `timeout-or-duplicate`. Claim the cache
atomically post-await and loop into a fresh mint if another caller won.
- usePackRequest: Drop pending pre-mint debounce timer at submitRequest
start and on unmount, and skip scheduling while loading is true. Stops
a debounce-firing-during-submit from minting an extra Turnstile
challenge alongside the click path's mint.
- TryItPackOptions: Emit userInput from option handlers and wire to
markUserTouched in TryIt. Without this, users hydrating via `?repo=`
who only tweak format/include patterns/checkboxes never tripped the
pre-mint gate, so their click path always cold-minted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
intent(fast-click-race): When the user clicked Pack within the 500 ms pre-mint debounce window, takeToken() cold-pathed into mintToken() *and* the debounce timer later fired preMintToken() which started a second mintToken(). The generation-counter supersede logic in mintToken() rejected the first call as "Superseded by new Turnstile request", so the user's own click surfaced as a verification failure even though Turnstile would have happily issued a token.
fix(unified-startMint): Extract a single `startMint()` that both takeToken (cold path) and preMintToken share. Concurrent calls return the same in-flight promise, so only one `turnstile.execute()` ever runs and the supersede branch only triggers when there is genuinely a stale request.
fix(takeToken-abort-race): The signal threading is now via a `waitWithAbort` helper that races the awaiter against the abort signal but lets the underlying mint keep going. If the user cancels mid-mint, the underlying challenge still runs to completion and the token lands in the cache for whoever submits next, instead of being thrown away.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
intent(latency-and-counter): The Cloudflare Turnstile dashboard's "challenges issued" counter sits at roughly 1.25× of GA page_view, which means simply loading `api.js` on every visitor (the PR #1541 pre-warm) is already inflating the counter — render() and execute() are not the only trigger. At the same time, click→token latency is the main remaining UX cost. This PR reshapes pre-warm so script load + challenge happen only when the user has shown real intent (filled a valid URL or chose a file *and* interacted with the form), achieving both a lower counter and a near-zero click→token latency.
fix(useTurnstile-api): Replace the single `getToken()` entry point with a `preMintToken()` / `takeToken()` pair backed by an in-memory `{ token, mintedAt, consumed }` cache. `preMintToken()` runs the challenge in the background and stashes the resulting token; `takeToken()` consumes the cache synchronously (instant submit) or awaits the in-flight mint, falling back to a cold mint with the supplied AbortSignal. `invalidateCache()` lets the caller drop the cache without minting a new token. TTL is bounded at 240 s — Cloudflare hard-caps tokens at 300 s, the margin absorbs clock skew and network round-trips so a token that's "almost expired" is never sent to siteverify.
fix(useTurnstile-no-mount-prewarm): Stop calling `loadTurnstileScript()` from `setContainer()`. The mount-time script load was the source of the page-view-shaped counter inflation. Pre-warm now only runs from the intent-gated trigger in `usePackRequest`, so visitors who never interact with the form never appear on the dashboard.
fix(usePackRequest-intent-gate): Add a `userTouched` ref that flips on real user interaction (input event, file upload, mode switch) and never goes back. A debounced (500 ms) `watch(isSubmitValid && userTouched)` calls `preMintToken()`, so URL-parameter hydration (`?repo=`), browser form restoration, and autofill never pre-mint. `submitRequest()` switches from `getToken()` to `takeToken()` so the cached token is consumed on the first click, with the cold mint path as a transparent fallback.
fix(TryItUrlInput-user-input-event): Emit a new `userInput` event from the URL field's actual `@input` handler. `TryIt.vue` wires it to `markUserTouched()`. Watching `inputUrl` directly would have re-fired during onMounted hydration and defeated the gate.
learned(cloudflare-counter-includes-script-load): Even with `execution: 'execute'`, the Cloudflare Turnstile dashboard counts api.js loads toward "challenges issued" (verified by comparing GA page_view ≈ 106 with CF issued ≈ 132 in the same 30 min window after the PR #1541 deploy). Treat any new place that loads api.js as a billable analytics side effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
intent(comment-drift): Claude's review surfaced three stale comments that no longer match the code after pre-warm was narrowed to the script-load step. No behaviour change — the comments were lying about what the code actually does now.
fix(useTurnstileScript-jsdoc): The JSDoc on `TurnstileRenderOptions.execution` claimed `'execute'` was chosen so the pre-warm path could render() without firing a challenge. PR #1541 proved that's not what `'execute'` does in practice — render() itself counts toward the dashboard's challenge counters, regardless of `execution`. Rewrote the comment to explain why we still pass `'execute'` (token-mint guardrail) and why we no longer pre-warm by rendering.
fix(useTurnstile-render-comment): The inline comment at the render() call site said this option is "what makes the pre-warm in setContainer() free of side-effects". setContainer() no longer calls render(), so the rationale is obsolete. Updated to describe the current role: a guardrail against an accidental render() minting a token before getToken() is ready.
fix(useTurnstile-race-comment): The single-flight cache comment said "pre-warm and getToken() can both race past the widgetId.value null check". Pre-warm doesn't enter ensureWidget anymore, so only back-to-back getToken() submits can race. Updated to reflect the narrower scope.
perf(preconnect-crossorigin): Add a `crossorigin` companion to the existing `<link rel="preconnect">`. Turnstile's `api.js` is fetched anonymously, but the Turnstile iframe issues CORS sub-requests on a separate browser connection pool. Without both hints, the iframe's first handshake still happens on click. Cheap defensive addition.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
intent(token-waste): Production telemetry (12-hour Cloudflare dashboard sample) showed 2,620 challenges issued and 986 solved against only ~150-200 actual pack clicks tracked in GA. Despite the widget being configured with `execution: 'execute'`, calling `turnstile.render()` at form-mount time was inflating the dashboard's challenge counters by every visitor — humans, crawlers, ad-blocked browsers, abandoned tabs. Cloudflare's docs say render() should be side-effect free in execute mode, but the analytics disagree.
fix(prewarm-scope): Drop the widget render from `setContainer()`. Pre-warm now only calls `loadTurnstileScript()` so the script is cached before the user clicks; the actual `turnstile.render()` happens on the first `getToken()` call. This restores the documented 1:1 relationship between solved challenges and actual pack submissions.
perf(preconnect): Add `preconnect` and `dns-prefetch` hints to challenges.cloudflare.com so the DNS / TLS / HTTP/2 handshake is warm before the click. Compensates for losing the render() pre-warm — the script load and the challenge round-trip both reuse the warmed connection.
learned(don't-trust-docs-when-telemetry-disagrees): When the dashboard says one thing and the docs say another, the dashboard wins. The previous PR added pre-warm believing render() was inert in execute mode; the analytics showed it wasn't. Going forward, treat any new render() call site as a billable side effect until proved otherwise on the dashboard.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address review feedback by inlining the bound abort functions directly
into setTimeout calls instead of using intermediate variables.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- usePackRequest.ts: Use `controller.abort.bind(controller, 'timeout')`
instead of an arrow function to avoid capturing the surrounding scope
- cache.ts: Save setInterval ID for cleanup, use `.bind()` instead of
arrow function, add `.unref()` to not block process exit, and add
`dispose()` method for proper resource cleanup
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement cancel button for pack requests with improved user experience.
The button now shows "Cancel" on hover during processing and allows users
to abort ongoing requests. Also improved error messages for better clarity.
- Add cancel functionality to PackButton with hover state
- Implement request cancellation using AbortController
- Improve error messages for remote repository processing failures
- Update timeout handling and server configuration
- Add proper event handling to prevent form submission conflicts
- Add onAbort callback to distinguish timeouts from errors
- Implement errorType system to differentiate warnings from errors
- Update TryItResultErrorContent to show warning styling for timeouts
- Change timeout message to suggest using Include/Ignore Patterns
- Timeout now displays with yellow warning icon instead of red error
- Replace forEach loops with for...of loops for better performance
- Fix import ordering and spacing inconsistencies
- Simplify interface definitions and type declarations
- Add proper padding to tokens column in file selection table
- Remove unnecessary line breaks in computed properties
- Improve code readability and consistency across components
- Convert file list to table format with clickable rows and compact layout
- Add warning message when selecting more than 500 files
- Increase include patterns limit from 1,000 to 100,000 characters
- Auto-switch to Result tab when Re-pack button is clicked
- Fix re-pack loading state to use shared loading status
- Sort files by token count instead of character count
- Replace Package icon with custom SVG for re-pack button
- Add row hover effects and full-row click selection
- Implement proper file selection state management during re-pack
- Add FileInfo interface for individual file metadata
- Extend PackResult to include allFiles array with complete file information
- Create TryItFileSelection component with checkboxes for each file
- Add bulk selection controls (Select All/Deselect All)
- Implement re-packing functionality for selected files only
- Add live statistics showing selected files and token counts
- Fix TypeScript configuration for proper import.meta.env support
- Add responsive design for mobile and desktop
- Include scrollable file list with proper overflow handling
Resolves the GitHub issue requesting checkboxes for file inclusion/exclusion
on the website UI. Provides tree-based selection interface after initial
packing process as suggested by maintainer.
# Conflicts:
# website/client/components/Home/TryItResult.vue
Centralize URL parameter parsing to improve efficiency and maintainability based on code review feedback.
Changes:
- Move URL parameter parsing to single location in usePackRequest onMounted
- Add applyUrlParameters function to usePackOptions for external use
- Remove duplicate parseUrlParameters calls (was called twice)
- Add detailed comments explaining SSR/hydration timing requirements
- Clean up imports in usePackOptions
This addresses review feedback from Gemini and Copilot while maintaining the SSR/hydration fix functionality.
URL parameters were not properly applied in production builds due to timing issues with SSR/hydration. This change moves URL parameter application to onMounted lifecycle to ensure proper initialization after the component is fully mounted on the client side.
Changes:
- Move URL parameter parsing from initialization to onMounted in usePackOptions
- Move repo parameter application to onMounted in usePackRequest
- Remove debug console logs
- Fix import statements for onMounted
This resolves the issue where format=markdown and other URL parameters worked in development but not in production builds.
- Replace 'any' with 'unknown' for better type safety
- Convert forEach to for...of loop for better performance
- Use proper type casting with unknown intermediate step
- Revert tooltip colors back to original styling (#333 background, white text)
- Fix all biome lint warnings
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Simplify shouldShowReset string comparison logic
- Use CSS variables for tooltip colors instead of hardcoded values
- Improve type safety in URL parameter parsing with proper type casting
- Remove redundant conditional in pack option comparison
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Implement comprehensive URL query parameter functionality for Repomix web interface
to enable easy sharing and bookmarking of configurations.
Features:
- Parse URL parameters on page load and apply settings automatically
- Update URL only when Pack button is pressed (not real-time)
- Exclude default values from URL parameters to keep URLs clean
- Reset functionality with icon-only button next to Pack button
- Hover tooltip for reset button explaining functionality
- Support for all pack options: format, patterns, boolean flags
- Auto-pack when valid repository URL is provided via parameters
Changes:
- Add urlParams.ts utility for parsing and updating URL parameters
- Update usePackOptions to initialize from URL parameters
- Modify usePackRequest to handle URL parameter logic
- Add reset button with tooltip next to Pack button (appears after Pack execution)
- Remove previous reset button from options panel
- Implement smart URL generation (only non-default values included)
Examples:
- Basic: ?repo=https://github.com/user/repo&format=markdown
- Advanced: ?repo=https://github.com/user/repo&include=**/*.cs&compress=trueResolves#764🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
User requested replacing JSZip with fflate in website/client:
- User asked: "website/clientでjszipではなくfflateを使うようにしてください。"
- Migrated from JSZip to fflate for smaller bundle size and better performance
- Updated useZipProcessor.ts to use fflate's functional API instead of JSZip's class-based API
- Maintained same functionality while improving efficiency
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
- Create useFileUpload composable for unified drag/drop, validation, and file processing
- Create useZipProcessor composable for ZIP file operations
- Refactor TryItFileUpload.vue: reduce from 213 to 105 lines (50% reduction)
- Refactor TryItFolderUpload.vue: reduce from 342 to 115 lines (66% reduction)
- Eliminate code duplication while maintaining all existing functionality
- Improve type safety with configurable validation and preprocessing pipelines
- Enable better reusability and testability of upload logic
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>