Adds the questionnaire and empty-state primitives and renders the
ask_user_question tool call inline in the chat, so the agent can offer choices
instead of guessing when a request is ambiguous.
The page now holds account-level actions rather than only destructive ones, so
it is reachable at /dashboard/settings/account and presented with a neutral
icon in the sidebar and command palette.
Swaps sonner's toast.success/error/loading/dismiss for the new toast.add({ type,
description }) and toast.close across dialogs, auth pages, the builder, the
dashboard and the applications views. Behaviour is unchanged.
Disabling the textarea for the duration of a response made the browser blur it,
so the caret left the composer on every send and had to be clicked back. send()
already ignores calls mid-stream, so Enter stays a no-op and type-ahead works.
Every step-start part serialises to the same JSON, so the content-derived key
collided for any multi-step assistant message and React warned about duplicate
keys on each incoming chunk. Two identical text parts collided the same way.
Parts are append-only and never reordered by the AI SDK, so the index is stable.
Public resume pages only produced their OpenGraph and Twitter tags client side,
so a shared link had no card at all. The server now injects them into the shell
and swaps in the resume's own title and description.
The lookup is scoped to public, password-free resumes and deliberately avoids
resumeService.getBySlug: that counts a view and would expose a protected
resume's summary to an unauthenticated crawler. User-authored values are escaped
before they reach the HTML, and any lookup failure falls back to the plain shell.
getResumeSocialMeta is shared with the client route head so the two cannot drift.
X reads twitter:* meta tags from the name attribute, not property, so the card
validator reported twitter:title and twitter:description as missing. Also adds
the og:type tag the root head was never emitting.
* feat(resume): add a deterministic ATS parseability check
Adds an offline ATS linter that reports whether a parser can read a
resume, surfaced as an always-on panel in the builder.
The existing Resume Analysis panel needs a configured AI provider, so
users who never set one up get no feedback at all. These 22 rules run
as a pure function over ResumeData with no provider, no network and no
rendered PDF, so they work for everyone on every edit.
Rules cover contact details, date parseability, sections that hold
content but never render, column and sidebar placement, and typography
thresholds. The catalog mirrors the Semantic CSS diagnostic catalog:
stable codes carrying a severity, meaning and action, with no i18n
dependency so the web layer translates by code. Each finding carries a
JSON Pointer, which is what makes jump-to-field work.
Deliberately no second score. Resume Analysis owns overallScore, so
this reports "N of M checks passed" and counts by severity instead.
* fix(resume): accept localized ongoing periods and reject bare ones
Two period-parsing bugs found in review.
The ongoing-token set was English-only, so a German resume reading
"2020 - heute" was reported as unparseable and the panel told the user
to rewrite a perfectly valid range. Rather than guess translations for
55 locales, a range ending that carries no digits and is not a month
name in the resume's locale is now read as ongoing. That keeps a
genuinely incomplete ending such as "Jan 2020 - Feb" reported, since
"Feb" resolves as a month.
A bare "Present" also parsed as a valid period, so an experience entry
with no start date passed the check. A standalone ongoing token is now
rejected; ongoing tokens remain valid as the end of a range.
* feat(web): scroll ATS findings to the item they belong to
Findings for different items in one section all landed on the section
header, so a date problem on the third role gave no more help than
naming the section.
getAtsFindingTarget now resolves the offending item from the finding's
JSON Pointer against the resume, and SectionItem carries a matching DOM
id. The panel scrolls to that item and falls back to the section header
when the item is not mounted, which is what happens while its section
is collapsed.
* fix(resume): recognize ongoing periods by token, not by shape
The previous heuristic read any short, digit-free range ending as an
ongoing marker, so "2020 - unknown", "2020 - later" and "2020 - tbd"
parsed cleanly and suppressed the finding they should have raised.
Replaced with an explicit table of ongoing words keyed by language,
covering the locales the app ships. Matching is exact, so unrecognized
endings are reported again. A locale missing from the table falls back
to the earlier behaviour of reporting its ongoing periods, which is a
visible gap someone can close by adding a word rather than a silent
hole in detection.
Tests assert every listed token parses and that the table stays
lowercase, since lookups normalize that way.
* fix(ats): parse punctuated ongoing tokens
* fix(ats): parse Unicode punctuated ongoing tokens
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
react-zoom-pan-pinch calls preventDefault() on its window-level mousedown
listener so that dragging the canvas does not select text. That also cancels the
browser's native focus shift, so focus stays wherever it was before the pan --
typically the sidebar button that opened the last dialog, since closing a dialog
restores focus to its trigger. A focused button activates on Space keyup, so
holding space to pan and then releasing it reopened the most recent dialog.
Blur the focused element from onPanningStart, which reinstates exactly the focus
change the browser would have made on its own. onPanningStart only fires when the
mousedown target is inside the transform wrapper, so sidebar and dialog clicks are
unaffected, and keyboard-only users never trigger a pointer pan.
Closes#3300
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* feat(applications): job application tracker with AI copilot
Add an Applications module at /dashboard/applications: pipeline board
(dnd-kit), table view with bulk actions, Insights (fit tiles, funnel,
sources, shareable funnel-flow SVG), campaigns, tags, CSV import, and
Add/Edit/Detail slide-overs. Each application links a live Reactive
Resume.
AI "Application Copilot" (applications.ai.*): job-posting autofill,
resume↔job match score (fit ring), resume tailoring, and cover-letter /
follow-up drafting — via the user's configured provider.
Board cards + table rows get context menus (edit / move / archive /
delete). Charts are CSS/SVG (no new chart dep); adds a UI Checkbox.
Also includes local TanStack devtools setup and toolchain bumps.
Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f
* feat(applications): close follow-up gaps + squash migrations
Finish the deferred/open items on the applications tracker:
- Cover-letter upload re-enabled. Fix the storage blocker by deriving the
key extension from content type (buildFileKey/EXTENSION_BY_CONTENT_TYPE)
instead of hardcoding .jpeg, so PDFs serve correctly and non-JPEG image
avatars keep working under FLAG_DISABLE_IMAGE_PROCESSING. Add
coverLetterUrl/coverLetterName columns + Documents-section upload/remove.
- Contacts editor in the detail sheet (add/edit/remove, keyed per app).
- Board caps rendered cards per column (COLUMN_PAGE_SIZE=50 + "Show more").
- Extract new Lingui messages across locales.
- Guard coverLetterUrl to http(s)/relative at the API boundary.
Squash the five branch-only application-table migrations (create -> +tags
-> +cover-letter -> drop -> re-add) into a single clean CREATE TABLE via
drizzle-kit generate.
Claude-Session: https://claude.ai/code/session_01TEeRHnEayw2MFCShFRyL5f
* chore: update dependencies
* fix(web): address React Doctor findings — compiler, purity, query, component structure
prefer-module-scope-pure-function: hoist buildSubtitle, getDecimalPlaces,
handleLocaleChange, onLocaleChange, stop, listContent/groupedListContent to
module scope so they aren't rebuilt on every render.
react-compiler-todo (??=): rewrite draft.metadata.styleRules ??= [] to the
non-assignment form to unblock auto-memoization.
set-state-in-effect: derive updatedAtLabel at render time instead of syncing
it through useState + useEffect.
query-destructure-result: destructure useQuery results at call site in
resume-analysis and resume-thumbnail to follow TanStack Query v5 convention.
only-export-components: extract non-component exports to sibling .ts files so
Fast Refresh can preserve component state:
- getNextWeights → typography/get-next-weights.ts
- detectJsonImportType + ImportType → dialogs/resume/import.utils.ts
- getLocaleOptions → features/locale/locale-options.tsx
- preview helpers + DEFAULT_PDF_PAGE_SIZE → preview.shared.utils.ts
- resolveHighlightToolbarState + defaultHighlightColor → rich-input.utils.ts
- computeDelta + getSparklinePoints → statistics.utils.ts
no-multi-comp: split multi-component files into focused companions:
- ResumePane + ToolbarButton → routes/agent/-components/resume-pane.tsx
- DesktopBuilderShell → builder/$resumeId/-components/desktop-builder-shell.tsx
- MobileBuilderShell + helpers → builder/$resumeId/-components/mobile-builder-shell.tsx
- setBuilderLayout/getBuilderLayout moved to -store/sidebar.ts
fix(tests): add Resume type import to section-builder mocks and cast partial
mock data as unknown as Resume to satisfy stricter type checking; fix
noExplicitAny Biome errors in the same mocks.
* feat(applications): improve performance
* chore: fix knip issues
* perf(builder): halve per-keystroke render cost
Section-form fields called `form.handleSubmit()` on every keystroke, which
re-validated the whole form and toggled submit state — firing the render
cascade twice per character (~6809 renders/keystroke, FPS dropping to 9).
Persist via a form-level `listeners.onChange` instead and drop the per-field
`handleSubmit()` (basics, custom-fields, design). Narrow header/dock resume
subscriptions to metadata slices so they no longer re-render on content edits.
Cuts renders 6809 -> 3403 per keystroke (50%), 0 frame drops. Save, preview,
and design controls verified working; 449/449 web tests pass.
* perf(home): eliminate hero CLS from unreserved video box
The hero <section> is `flex items-center` (shrink-to-fit), so the video
wrapper's width depended on the video's intrinsic size, which only resolves
after the media loads. aspect-ratio couldn't reserve height without a definite
width, so the video grew from ~190px to ~563px after first paint and shoved the
centered hero text down ~373px (CLS ~0.095).
Give the wrapper a definite width (w-full + mx-auto on the CometCard) and set an
explicit aspect ratio + width/height on the video so its box is reserved before
load. CLS 0.095 -> 0; hero stays visually centered at max-w-4xl.
* docs: add application tracker guides
* chore(db): squash application migrations
* fix(email): import React in auth template for server-side rendering compatibility
* chore(release): v5.2.1
* Refactor resume rendering and builder workflows
* fix: address application tracker review findings
* feat(export): separate resume/cover-letter downloads, redesign dialog, add Markdown
Let people export the resume and cover letter as distinct documents, and add a
Markdown format alongside PDF / DOCX / JSON (handy for AI agents).
- Server/API: scope PDF generation and download URLs to a resume/cover-letter target.
- Export domain: getResumeExportData + resumeHasCoverLetter in @reactive-resume/resume.
- Redesign the download dialog: one global "What to export" scope toggle (Tabs) plus
flattened per-format rows, reusing existing UI components and design language.
- Add Markdown export (@reactive-resume/resume/markdown) with a small tiptap-HTML converter.
- Fix blank section headings in DOCX and Markdown by injecting the locale-aware
section-title resolver (titles are stored empty and resolved at render time).
- Locale catalogs updated for the new strings.
* test(e2e): open the download dialog before exporting JSON
The JSON export moved into the redesigned download dialog, so the spec now opens
the dialog from the Export sidebar section before clicking "Download JSON".
Number(null) === 0 caused the old code to use a separate :initialized key as
a migration sentinel. A plain null check on localStorage.getItem() is
sufficient and removes the sentinel key entirely.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Extract useTypographyForm helper (captures form creation + sync) and
TypographyForm type. Extract TypographyGroupFields component with
prefix "body" | "heading" to replace 2x4 duplicated form.Field blocks.
Font Weight label ("Font Weights" vs "Font Weight") is preserved per
prefix.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
useInitializeResumeStore, useMergeResumeMetadata, useSaveStatus,
useCanUndo, useCanRedo, useUndoResume, useRedoResume each had exactly
one caller. Inline useResumeStore selectors at call sites and remove
the wrappers. Skipped usePatchResume (4 callers).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
The uploads endpoint is public (Cache-Control: public, no auth headers),
so a plain img src suffices. Remove createPicturePreviewUrl, the useQuery
call, and the object-URL cleanup useEffect; simplify PicturePreviewControls
to use normalizedPictureUrl directly.
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Remove generic selector overload from useBuilderSidebar; callers
destructure the full return object instead of using a selector that
provides no meaningful benefit (state is rebuilt on every render).
Claude-Session: https://claude.ai/code/session_012Bnvt1MghwHj4qQRxuQUGa
Prevent the sponsor logo images from intercepting clicks so the
anchor reliably opens atlascloud.ai in a new tab instead of the SVG
asset.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
* feat: add section heading icons to PDF templates
Add customizable Phosphor icons before section titles in PDF output.
Users can toggle visibility globally via a new "Hide section heading icons"
switch (independent of item-level icons) and customize individual section
icons through the builder sidebar icon picker.
- Add `icon` field to `baseSectionSchema` and `summarySchema`
- Add `hideSectionIcons` to `pageSchema` (defaults to true for backward compat)
- Implement `SectionHeadingIcon` component with heading font-size scaling
- Support "none" sentinel for per-section icon hiding
- Fallback to sensible defaults (briefcase, graduation-cap, etc.) for legacy data
- Add icon picker to builder sidebar sections and custom section dialogs
Closes#2632
* test: add unit tests for section heading icons
- Add tests for getResumeSectionIcon() covering built-in sections,
summary, custom sections, "none" sentinel, and default fallbacks
- Add schema tests for baseSectionSchema icon field, summarySchema icon,
and pageSchema hideSectionIcons default behavior
* refactor: minor updates to icon display
* Update apps/web/locales/es-ES.po
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
* fix(pdf): apply custom style fontSize to icon and level indicator sizes
Map fontSize from Icon and Level Indicator custom style slots to Phosphor
icon size and level indicator dimensions, since react-pdf icons ignore
fontSize in favor of the size prop.
* fix: separate global icon and scoped level indicator font sizes
Icon slot fontSize now drives all resume icons plus level display
decorations. Level indicator fontSize overrides only within level display.
Shared sizing logic lives in schema; design sidebar preview uses global rules.
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>