* feat(import): parse a PDF resume without an AI provider
Importing a PDF required a connected AI provider, so anyone without a
paid API key could only import the three JSON formats. Almost nobody
arrives with one of those files; they arrive with a PDF. The first thing
a new user tries to do was blocked behind bringing their own key.
Adds a deterministic parser that reads the text out of the PDF in the
browser and prefills the builder. It pulls the contact block, segments
the body on conventional headings, and maps entries to real items,
reusing the ATS period parser for dates so a date range is not mistaken
for a phone number.
Nothing is thrown away: header parts that do not map to a field go into
the description, and unrecognized headings become custom sections. The
imported sections are placed on the page so the result renders straight
away. Output is validated against the resume schema before it is
returned.
Text extraction groups items by baseline rather than trusting hasEOL,
and turns wide column gaps into a double space, which is what lets a
row split into company, position and location.
The AI path still runs when a provider is connected. Word import is
unchanged and still requires one.
Closes#3334
* fix(import): keep every section and entry the PDF actually contains
Review found three ways the parser lost or mangled content, all of them
reproducible.
A document whose first heading was not one of the known aliases never
started a section, because unknown-heading detection was gated on a
section already being open. Everything after it was swallowed as contact
header text. The header block is now bounded by where the contact
details stop, so a heading is recognized wherever it appears.
An entry spreading company, position and dates over three lines was
imported as two malformed items. A line that introduces an entry now
merges into the open entry instead of starting a second one.
An uppercase company such as ACME CORPORATION was read as a section
heading and fragmented the entry. A heading candidate followed by a date
line is now treated as an entry header, which is what it is.
Also escape single quotes, and construct the PDF worker inside the try
so the nested worker is terminated even if construction throws.
Title-case headings are deliberately still not treated as headings:
company and school names are title case too, and splitting on them would
fragment real entries. Such a section stays in the preceding one with its
text intact rather than risking loss.
* fix(import): look past a multi-line preamble before calling a line a heading
The previous guard only inspected the next line, so an uppercase company
followed by a separate role line and then the dates was still read as a
section heading. The experience or education entry was moved into a
custom section and lost.
Heading detection now scans a two-line window for the date that marks an
entry, and stops early at a bullet so a genuine heading whose section
opens with bullet points is still recognized.
The window can suppress a real heading whose first entry puts a bare date
two lines below it. That is the deliberate direction to fail in: a missed
heading leaves the text in the preceding section, while a misread entry
fragments structured content.
* fix(import): collect an entry preamble until its dates appear
An entry that spread company, role, location and dates over four lines
was imported as two broken items: the company with no dates, and the
location carrying the period.
The cause was in entry grouping rather than heading detection. Lines
before a date were only folded into the entry header when the date sat
on the very next line; anything earlier fell through to the description.
Preamble lines are now collected into the entry header until the dates
turn up, bounded by the same lookahead and stopping at a bullet, so an
undated section cannot swallow itself.
The heading lookahead widens to four lines to match, which is the
realistic maximum for company, role, location and dates.
* fix(import): harden local PDF resume parsing
* chore(import): document audited HTML construction
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* feat(skills): add inline layout option for skill items
* fix: restore default skills layout (regressed by inline feature)
- Restore metrics rowGap style for default layout
- Only render LevelDisplay inside the row for inline layout, not default
* refactor(pdf): Extract inline skills style logic from JSX to reusable function
* test(pdf): add test coverage for inline skills item layout
- Add test suite SkillsSectionInlineFormat to verify isInlineSkillsItem and getSkillsItemStyle behavior
* test(pdf): add comprehensive test coverage for inline skills item style logic
- Test combinations of proficiency, level, and keywords fields (0, 1, 3 fields)
* test(schema): add test coverage for column equals 1 when layout is inline
* test(web): add component-level tests for inline and columns layouts
* test(import): add v4 parser-level test for missing skills layout
* docs: regenerate skills layout references
* test(docx): include skills layout in section fixtures
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(pdf): keep list markers with their first text fragment
* fix(pdf): preserve page breaks while rewinding list companions
* fix(pdf): consume oversized list marker presence hints
* test(pdf): allow cold startup for pagination process guard
* fix(pdf): key list presence spacer
* fix(ai): make provider test timeout configurable via AI_TEST_TIMEOUT_MS
- Problem: the 30s hardcoded timeout is too short for self-hosted
deployments with cold-start models (e.g. Ollama). Makes it impossible
to pass the provider test (issue #3374).
- Fix: read AI_TEST_TIMEOUT_MS from the environment, defaulting to 30_000.
Zero behaviour change when the env var is absent.
- Verification: existing test asserts "30 seconds" in the timeout
message; default is unchanged so the test continues to pass.
(CI needs Node 22+ — not available on this host.)
* fix(ai): add AI_TEST_TIMEOUT_MS to turbo globalEnv so it reaches the API process
- Problem: Turborepo filters env vars not listed in globalEnv, so
AI_TEST_TIMEOUT_MS would always be undefined at runtime under
turbo dev/start, making the override dead code.
- Fix: add AI_TEST_TIMEOUT_MS to the globalEnv array.
- Verification: turbo.json validates as valid JSON.
* fix(ai): validate AI_TEST_TIMEOUT_MS as a finite non-negative integer
* docs(ai): add JSDoc to timeout parser and test helper
* test(ai): restore AI_TEST_TIMEOUT_MS after timeout tests
- Problem: loadWithTimeout() mutates process.env.AI_TEST_TIMEOUT_MS but nothing restores it, so the last value tested ("999999999999") leaked to every test that runs after this describe block in the same file.
- Fix: save the pre-test value and restore it in an afterEach hook.
- Verification: pnpm exec vitest run src/features/ai/service.test.ts in packages/api — 18/18 passed.
* test(api): isolate AI timeout environment cases
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(pdf): add left padding to section heading text to prevent first-character clipping
Closes#3380
* fix(pdf): apply heading padding default after style composition
Apply paddingLeft: 1 only when no composed style fragment already defines it, so an explicit paddingLeft from a template or style rule is preserved. Keep the fallback for an empty style list.
* fix(pdf): keep heading safety padding on text only
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(components/form): resolve FormControl label target regressions (#3369)
- Expose FormControlContext and wrap FormControl children in Base UI's
LabelableProvider so the generated control id reaches the actual
labelable element.
- Update InputGroup/InputGroupInput to consume the context and place
the id on the real input instead of the fieldset.
- Update Slider to discard the wrapper id and use the context via
LabelableProvider so the thumb input receives the id and
aria-labelledby.
- Update ChipInput to consume the context, set id and aria-labelledby
on the inner input, and only fall back to aria-label when not inside
a FormItem.
- Restructure the sidebar layout so a single FormControl labels the
numeric input and the visible FormLabel is referenced by id for the
sibling Slider, removing the duplicate-id defect.
- Add a dev-time warning when the generated id lands on a non-labelable
or missing element.
- Extend form.test.tsx with regression coverage.
* test(form): add regression coverage for chip-input and dual-control layout
* fix(ui): surface FormControl error state as aria-invalid on the Slider control
- Problem: FormControl injects aria-invalid={hasError} onto its rendered
element, but Slider stripped it without re-applying it anywhere, so the
error state never reached the DOM (flagged by Codacy/Greptile/CodeRabbit).
- Fix: bridge aria-invalid onto Base UI's native range input via the Thumb's
public inputRef prop; Base UI v1.7 has no prop path for it (its validation
props only apply through Base UI Field context). id stays stripped since
LabelableProvider already delivers it to the input.
- Verification: new regression test in form.test.tsx fails on the pre-fix
head (aria-invalid null) and passes post-fix; packages/ui 363/363 tests
green; tsc --noEmit on packages/ui clean.
* fix(ui): let a caller-supplied data-slot override the Slider default
- Problem: the FormControl label-target fix moved data-slot="slider" after
{...props} on SliderPrimitive.Root, so a caller's data-slot was silently
overwritten with the default — a prop-ordering regression against both the
prior file and the repo-wide convention (FormItem, FormLabel, InputGroup all
place data-slot before the spread).
- Fix: restore data-slot="slider" before {...props} so caller values win.
- Verification: packages/ui — vitest src/components/slider.test.tsx
src/components/form.test.tsx = 30/30 passing; new regression test
("lets a caller-supplied data-slot override the default") fails on the
pre-fix head (data-slot="slider" wins) and passes with the fix; tsc
--noEmit clean.
* fix(ui): preserve standalone Slider and InputGroup identity props
- Problem: the FormControl prop strip dropped a standalone caller's id on
Slider and id/aria-describedby/aria-invalid on InputGroup, so standalone
compositions rendered no element carrying those attributes (regression
vs main, flagged by maintainer review on this PR).
- Fix: strip the FormControl-generated props only when a FormControl
ancestor is present (useFormControl context); preserve explicit caller
props for standalone usage in both components.
- Verification: new standalone + FormControl-wrapped tests fail on the
prior head and pass after the fix; packages/ui 367/367, apps/web
595/595, tsgo --noEmit clean.
* fix(ui): remove internal label provider dependency
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(api): translate copilot AI provider failures to BAD_GATEWAY
- Problem: AI provider errors (bad key, unknown model, quota, 5xx) from the
AI SDK bubble out as opaque 500 INTERNAL_SERVER_ERROR from copilot
endpoints (autofill, match-score, draft-message, tailor-resume).
- Fix: catch AISDKError in generatePlainText and a local generateJson
wrapper that delegates to the shared generate-json module, translating
both to BAD_GATEWAY (502) with the original error preserved as cause.
Mirrors the existing pattern in features/ai/router.ts.
- Verification: vitest (CI — requires Node 22+). Test file unchanged in
assertion logic from the original PR; the local generateJson now
wraps the shared module instead of duplicating it.
Rebased onto main after v5.2.9 AI-layer refactor (generateJson extracted
into features/ai/generate-json.ts).
* fix(api): align generateJson prompt shape with callers and shared module
- Problem: local generateJson wrapper accepted (model, prompt: string,
schema) but all callers pass (model, { prompt: string }, schema).
Caught by CodeRabbit review.
- Fix: match the shared generate-json module signature — accept
{ system?, prompt } as the second argument and pass it through.
Updated test calls to match.
* fix(test): remove stray leading dots from mock object property names
- Problem: rebase onto v5.2.9 introduced `.use`, `.output`, `.errors`
as property names in the chain mock object, which is invalid JS
syntax and would cause a parse error when tests run.
- Fix: remove the leading dots to restore valid property names.
- Verification: cat -A confirms tabs-only indentation, no leading dots.
* fix(docs): correct 'a actionable' to 'an actionable' in comment
- Problem: Grammar typo in inline comment.
- Fix: 'a actionable' → 'an actionable'.
- Verification: grep confirms no remaining instances.
* fix(api): narrow copilot AI BAD_GATEWAY predicate to APICallError and exhausted RetryError
Rewrites the landing page, in-app microcopy, and public docs, then fixes what the rewrite exposed: stale template counts, a broken quickstart anchor, out-of-sync FAQ structured data, dead error-hint branches in the MCP tools, and wrong-sense translations across all 53 locales. Adds GLOSSARY.md so translators get the right sense of the ambiguous UI terms.
* feat(ats): add ATS checker and replace resume analysis
Adds a public, browser-only ATS checker at /ats-checker and an ATS Check
section in the builder's right sidebar. PDFs are parsed locally: text
extraction, reading order, contact and date recovery, section detection,
and file-level readability are scored deterministically, with evidence
cited per finding and skipped checks reported rather than counted as
passes.
Removes the AI-scored resume analysis it supersedes: the resume_analysis
table (dropped via migration), the get_resume_analysis MCP tool, and
POST /ai/analyze-resume. The replacement, POST /ai/ats-review, reviews
extracted resume text and returns qualitative feedback with no score.
Also bumps the version to 5.2.9 and adds the changelog entry.
* chore(deps): bump workspace dependencies
* fix(ats-checker): keep negation inside each 'what this does not do' bullet
The three bullets were bare fragments whose negation came from the
section heading, which translators never see. A dozen locales rendered
them as affirmative assertions or imperatives, so the page claimed the
checker enforces a one-page rule and predicts rejection -- the opposite
of the source, and directly contradicted by the sentence beside it.
Each bullet now carries its own negation, so the polarity cannot be
lost in translation. Re-extracted and refilled across all 53 target
locales.
* fix(pdf): resolve bold text weight from the family's bold face
Bold text (<strong>, rich-text bold, template bold styles) previously
rendered at the last stored body weight, which is ambiguous: families
are commonly stored as ["400","600"] (the typography picker's default
pairing), so bold rendered at SemiBold — nearly indistinguishable from
Regular for faces like Open Sans (#3310).
Add resolveBoldFontWeight() to the fonts package: keep a deliberate
stored bold-class choice (>= 700), else prefer the family's true Bold
face ("700"), else the heaviest >= 600 face; return null so callers
keep their existing fallback when the family has no bold-class face.
Wire it through use-register-fonts, the shared base-template-styles
builder, base-styles and the Scizor template. Default body IBM Plex
Serif ["400","500"] now renders bold at 700 (base-reset-fidelity
expectation updated accordingly).
Fixes#3310
* fix(pdf): register bold fallback faces for CJK glyph substitution
When resolveBoldFontWeight maps stored weights like ["400","600"] to the
family's 700 face, register that weight on each PDF fallback font too so
glyph-level substitution keeps bold glyphs instead of snapping to 600.
Also reorder @reactive-resume/fonts imports per Biome convention.
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
* fix(pdf): render emoji via a Noto Emoji script fallback
Emoji in resume content (flags, globe, pictographs) rendered as mojibake
in the preview and PDF export because the per-codepoint fallback chain
registered no emoji-capable font: every font in the stack lacked the
glyphs, so layout fell through to single-byte standard-font encoding —
each UTF-16 code unit truncated to its low byte (#3321).
Follows the #2986/#3190 script-fallback pattern: detect emoji content
(regional indicators unioned with Extended_Pictographic), map it to the
monochrome Noto Emoji web font (TrueType glyf outlines, PDF-embeddable),
and register it in the fallback stack for both serif and sans stacks.
Out-of-range weight requests alias to the nearest served weight (300-700)
so registration never falls back to the preview subset.
* fix(pdf): detect keycap emoji via the combining enclosing keycap
Greptile review on #3351: keycap sequences like 1\uFE0F\u20E3 carry no
regional indicator and no Extended_Pictographic codepoint, so they
bypassed the emoji detector and rendered garbled — the exact class of
bug #3321 fixes. Union U+20E3 into the detector; every valid keycap
sequence contains it.
* Update packages/utils/src/locale.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* [autofix.ci] apply automated fixes
---------
Co-authored-by: Amruth Pillai <im.amruth@gmail.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* fix(fonts): register Vazirmatn in webfont catalog for JSON imports
Imported resumes can set typography.fontFamily to Vazirmatn, but the
popularity-sorted Google Fonts slice omits it so PDF registration fell
back to IBM Plex Serif and Persian/Arabic glyphs stacked or tofu (#3098).
Add Vazirmatn as a locale-coverage manual entry (same pattern as Carlito)
and cover catalog resolution with unit tests.
* test(pdf): keep Vazirmatn as primary family for fa-IR registration
Prove JSON-imported Vazirmatn is handed to Font.register instead of
being rewritten to IBM Plex Serif (#3098).
* fix(fonts): address CodeRabbit review on Vazirmatn catalog
Assert getWebFontSource resolves files["400"] for Vazirmatn instead of
only matching the preview fallback, and split the font-generation log
line to satisfy the 120-column Biome limit.
Change richListItemContent from flex: initial to flex: 1 with minWidth: 0.
This makes the content area fill remaining space after the marker and
columnGap, so text wraps within the user-set margin instead of overflowing.
Fixes#3336
Upgrades to Better Auth 1.7, expands Custom Styles coverage of item headers, and adds a human-approval step to the AI agent's resume edits.
Breaking for self-hosters using a custom OAuth provider: the callback path changes from /api/auth/oauth2/callback/custom to /api/auth/callback/custom, and installs using OAUTH_DISCOVERY_URL need one additional UPDATE after upgrading. Both are documented in docs/self-hosting/sso.mdx.
- Better Auth 1.7, with the account issuer migration and the jwks alg/crv columns the 1.7 jwt plugin requires
- Agent edits gated behind an approval step, with crash-safe runs and context pruning
- item-header now covers every section header row on every template; adds the item-header-row part
- Fixes provider unlinking, auth error messages, and version conflicts on freshly created resumes
- New /auth/error page, translated across all 53 target locales
- DeepSeek Harness plugin moved into packages/dsh-plugin
- Dependency bumps across the workspace
Postgres defaultNow() stores microseconds while JS Dates are millisecond-truncated, so the SQL equality guard matched zero rows on freshly created resumes and every guarded agent patch failed with a permanent version conflict. The SELECT ... FOR UPDATE lock plus the in-transaction ms-precision check already provide the guarantee; drop the SQL predicate. Verified A/B against a live database.
Better Auth guards `/unlink-account` with `freshSessionMiddleware`, which
rejects any session whose `createdAt` is older than `freshAge` (one day by
default). Sessions here last a week and there is no re-authentication flow to
refresh that timestamp, so disconnecting a provider failed with
`SESSION_NOT_FRESH` for every user who signed in more than a day ago.
Disable the freshness gate, and teach `getReadableErrorMessage` to read plain
error objects: Better Auth client errors are `{ code, message, status }`
objects rather than `Error` instances, so every auth toast was collapsing to
its generic fallback instead of showing the real reason.
* docs(adr): propose agent AI SDK v7 adoption plan
* fix(ai): bind analyzeResume through aiService in service test
The test destructured analyzeResume as a named export that does not exist; main was red.
* test(agent): keep pure ai helpers real via spread-actual mock factory
* feat(agent): add run guards, patch version guard, run wall-clock timeout
* feat(agent): validate UI messages at the send boundary
* feat(agent): crash-safe draft-row persistence and server-side cancellation
* feat(agent): reap stale run claims at boot, on send, and on thread open
* feat(agent): fresh-document patch output and tiered context pruning
* feat(ai): shared agent tool contracts and message metadata schema
* feat(agent): add per-thread review-patches setting with update endpoint
* feat(agent): gate resume patches behind hmac-signed tool approval
* feat(agent): merge question answers and approval decisions before run claim
* feat(agent): approval ui with composed auto-send and fixture-driven tests
* feat(agent): usage metadata, tool activity cards, smoother streaming
* feat(agent): tool-call repair, input examples, structured step logging
* chore(i18n): translate new agent workspace strings across all locales
* fix(agent): gate stale-run draft cancellation on winning the claim clear
Snapshot streaming drafts before the conditional clear and skip the flip entirely when another reaper or a replacement run already cleared the claim. Also address review nits in eleven locale catalogs.
* fix(agent): flip reaped drafts only when their snapshotted state is unchanged
* fix(agent): address review findings across run lifecycle, context budget, and approval flow
- bind patches to the revision the model read via signed baseUpdatedAt
- claim the run before consuming a continuation; recorded-but-unexecuted approvals retry as pending continuations
- keep run ownership on stop() until cancellation persists; preserve the claim for the reaper when final persistence fails
- estimate tokens without serializing binary attachments (tokenx) and enforce the budget by dropping oldest whole turns
- mark crash-recovered patch results as snapshot boundaries; strip /data prefixes at execution time
- retry failed continuations without regenerate; mount a single AgentChat; disable response controls on read-only threads; freeze review toggle during runs (client+server)
- accumulate usage across continuations and match the SDK's nested usage shape; label-form token strings; reorderable source label; accessible note field; state-neutral web-search label
* chore(i18n): translate revised agent strings across all locales
* fix(agent): harden baseUpdatedAt validation and address review follow-ups
- bundle tokenx in the server runtime dependencies (e2e boot failure)
- strict ISO schema for baseUpdatedAt plus loud executor rejection of unparseable values
- it-IT source label consistency (Fonte)
- prove penultimate-turn retention in the context pruning test
* chore(deps): exempt tokenx from knip for the externalized server bundle
`SectionItemHeader` only rendered its own box when a template opted into
`mainItemHeaderBorder` (only Ditgar did). Everywhere else it walked the
header children and attached the resolved `item-header` style to the first
descendant that happened to be a literal `View` or `InlineItemHeader`.
Sections whose header starts with anything else — certifications, awards,
projects, publications, references — matched nothing, so the style was
silently dropped; stacked headers such as experience matched only their
first row, so a second row went unstyled.
The header now always renders its own `Div`, so `item-header` covers the
whole header row of every section on every template. `Div` rather than
`View` keeps the base row gap the rows used to inherit from the item box,
and Ditgar keeps its tight header via `rowGap: 0` on its own
`sectionItemHeader` slot, so rendered output is unchanged apart from the
newly styled rows. `mainItemHeaderBorder` is now dead and removed.
Fixes#3349
* docs: remove .superpowers
* feat(dsh-plugin): bring the DeepSeek Harness plugin into the monorepo
Moves dsh-plugin-reactive-resume out of its own repository and into
packages/dsh-plugin. It stays a published, public npm package — the only
one here — but now builds, typechecks, tests, and lints under the same
turbo tasks as everything else.
The move pays for itself in the drift guard. Standalone, the plugin kept a
generated snapshot of the tool names scraped from the live server card at
https://rxresu.me, plus a weekly CI job to notice when that snapshot went
stale. Sitting next to packages/mcp, it reads MCP_TOOL_NAME directly, so a
tool rename breaks the prompt guide on the same pull request instead of
days later. The snapshot, the fetch script, and the scheduled job are gone.
packages/mcp gains a ./tool-names export so that import goes through the
public export map rather than another workspace's src.
Also flips autoInstallPeers off. The DeepSeek Harness rc packages declare
peers that are host-supplied and, in one case
(@deepseek-ai/dsh-type-meta), not published at all, so auto-install 404s
the whole workspace. Turning it off drops only optional peers elsewhere;
@neodrag/core was the single hard peer that had been arriving implicitly,
and it is now declared where it is used. Full typecheck and test suites
pass, and pnpm peers check reports nothing new beyond the pre-existing
drizzle-orm range mismatch.
Tests move from test/ to colocated src/*.test.ts and the build output from
lib/ to dist/ to match repository conventions.
* fix(dsh-plugin): ship a bundle manifest and target the current Harness
`dsh plugin add` warned that the package "declares no dsh.bundle — installed
as a plain dependency, not a profile layer", and it was right. Every other
Harness plugin, in-box and third-party, ships a cordis.patch.yml and points
dsh.bundle.patch at it; that declaration is what joins a package to a
profile's bundle stack. Without it the package installed and then sat inert,
and the README's hand-written insert row was a workaround for the gap rather
than the intended way in.
The peer ranges were also a generation behind. They asked for
@deepseek-ai/dsh-mcp-client and dsh-system-prompt at ^0.0.1-rc.1, which
cannot match the 0.1.0-rc.6 a current harness ships, so the plugin could
never have resolved against the thing it targets. Both APIs are unchanged
across the bump — StreamableHttpConfig still takes the same six fields and
PromptSection still takes name/order/text — so this is a range correction,
not a migration.
That bump pays for itself elsewhere. The old generation peer-depended on
@deepseek-ai/dsh-type-meta, which was never published, and working around
that 404 is why merging this package turned autoInstallPeers off for the
whole repository and pulled @neodrag/core in by hand. The new generation
dropped that peer and publishes every other one, so both changes are
reverted and pnpm-workspace.yaml is back to what it was.
Because a bundle patch mounts the plugin the moment it is installed, a
required apiKey would fail config validation and take the profile down
before the user ever had a chance to mint a key. It now defaults to empty
and apply() warns and mounts nothing, matching how dsh-honcho-memory
handles the same problem.
Verified by packing the tarball and installing it into a clean project with
default pnpm settings: it resolves, imports, and reports its exports.
Section item headers render the title and its trailing date inside a shared
split row styled with `flex-wrap: wrap`. When the title is long the date wraps
onto its own line and left-aligns instead of staying pinned right, which reads
as inconsistent down a list of certifications.
That row had no selector. It is a bare View, so it never reached the semantic
tree: `item-header` matches the box around the row, and the title and date are
text nodes that layout properties do not apply to. There was no stylesheet that
could reach it.
Expose it as `template-part[name="item-header-row"]`, shared by every template
because the row comes from the shared section components. Awards,
certifications, projects and publications are covered; experience, education
and volunteer stack two rows and hand their headers to the
`inline-item-header-*` parts on some templates, so they are left alone.
Readers can now write:
@version 1;
template-part[name="item-header-row"] { flex-wrap: nowrap; }
The sonner wrapper was the only consumer of next-themes; the Base UI toast
that replaced it does not use the hook. knip flagged it as unused, and CI's
`knip --fix` step removed it and then failed `pnpm check` against a lockfile
that still listed it.
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.
Fetching an arbitrary job URL server side meant owning SSRF defence, redirect
and size limits, and per-site scraping quirks. The autofill tool now takes only
pasted text, so the URL input, the fetch path and its MCP annotation are gone.
The sheet gates the call behind a tested AI provider and a minimum paste length
so a stray snippet does not spend an AI call.