feat(form-assistant): local Grammarly-like field assistant (Phase 1) - #277
Conversation
Introduces a content-script-based form assistant that detects eligible inputs on known connector platforms and proposes field values derived from the local user profile via Gemini Nano (on-device, free). Phase 1 is local-only with no backend; remote Eve engine is reserved for Phase 2. Core (pure, no I/O): - types/classify-field/sanitize-field-descriptor/select-engine - build-field-prompt/parse-field-proposal/redact-for-remote - 45 mock-free unit tests Shell: - settings.ts: isolated chrome.storage.local key (formAssist) - local-generator.ts: Gemini Nano proposal (8s timeout + retry) - 3 SW handlers (Machine B, local path only) - bridge messages + Zod .strict() (7 FORM_ASSIST_* messages) Content script (Machine A): - field-detector: DOM -> FieldDescriptor pipeline - widget: closed Shadow DOM, 4 states, viewport-aware anchoring - index.ts: focus detection, bridge, native-setter applyValue (React/Svelte compatible) Wiring: - meta.ts: formAssist?: boolean (Free-Work pilot opt-in) - vite.config.ts: content_scripts.matches derived from catalog - dev stubs: storage key + mocked proposals per FieldKind Models/specs: - form-assistant.model.md (3 machines A/B/C), review matrix, openspec proposal Follows Model -> Review -> Implement -> Verify. The LLM produces signals; the model decides. No auto-submission: values are applied only on explicit user acceptance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
There was a problem hiding this comment.
Pull request overview
Adds Phase 1 of a local-first “Grammarly-like” Form Assistant for mission-platform application forms. It introduces a pure Core module for field classification/sanitization/prompting/parsing, a content script widget + orchestrator, and service-worker handlers to generate proposals via on-device Gemini Nano, while keeping the model/specs as the source of truth.
Changes:
- Introduces
core/form-assistant/*(pure) with field kind detection, sanitization, engine selection, prompt building, output parsing, and remote redaction modeling. - Wires a new content script (
src/content/form-assistant/*) + manifestcontent_scripts.matchesderived from connector catalog opt-in (formAssist: true). - Adds SW message handlers + schemas, isolated
chrome.storage.localsettings key, dev stubs, and unit tests for the new Core behaviors.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| openspec/changes/form-assistant/proposal.md | OpenSpec proposal for the feature scope, invariants, and rollout plan |
| apps/extension/vite.config.ts | Derives manifest content_scripts matches from connector catalog opt-in |
| apps/extension/tests/unit/form-assistant/select-engine.test.ts | Unit tests for engine-selection truth table |
| apps/extension/tests/unit/form-assistant/sanitize-field-descriptor.test.ts | Unit tests for PII stripping + normalization/limits |
| apps/extension/tests/unit/form-assistant/redact-for-remote.test.ts | Unit tests for allowlisted remote request projection |
| apps/extension/tests/unit/form-assistant/parse-field-proposal.test.ts | Unit tests for defensive parsing/truncation behavior |
| apps/extension/tests/unit/form-assistant/classify-field.test.ts | Unit tests for field classification rules |
| apps/extension/src/models/form-assistant.review.md | Model review coverage matrix + residual risks |
| apps/extension/src/models/form-assistant.model.md | Authoritative state-machine + invariants/spec for A/B/C machines |
| apps/extension/src/lib/shell/messaging/schemas.ts | Adds Zod schemas for FORM_ASSIST_* messages and FieldDescriptor |
| apps/extension/src/lib/shell/messaging/bridge.ts | Adds bridge message types for form-assistant flow |
| apps/extension/src/lib/shell/form-assistant/settings.ts | Isolated chrome.storage.local settings for enabling assistant |
| apps/extension/src/lib/shell/form-assistant/local-generator.ts | Gemini Nano proposal generator with timeout/retry + core prompt/parse |
| apps/extension/src/lib/shell/connectors/meta.ts | Adds formAssist?: boolean opt-in flag (Free-Work pilot enabled) |
| apps/extension/src/lib/core/form-assistant/types.ts | Core types for descriptors, engine selection, and remote request shape |
| apps/extension/src/lib/core/form-assistant/select-engine.ts | Pure engine selection logic (truth-table style) |
| apps/extension/src/lib/core/form-assistant/sanitize-field-descriptor.ts | Pure sanitization (PII removal, normalization, length caps) |
| apps/extension/src/lib/core/form-assistant/redact-for-remote.ts | Pure allowlisted profile projection for Phase 2 remote path |
| apps/extension/src/lib/core/form-assistant/parse-field-proposal.ts | Pure parsing/normalization of model output |
| apps/extension/src/lib/core/form-assistant/index.ts | Barrel exports for form-assistant core |
| apps/extension/src/lib/core/form-assistant/classify-field.ts | Pure field-kind detection based on label/placeholder/input type |
| apps/extension/src/lib/core/form-assistant/build-field-prompt.ts | Pure prompt construction for local generator |
| apps/extension/src/dev/chrome-stubs.ts | Dev-mode storage + canned assistant responses for UX/contract testing |
| apps/extension/src/content/form-assistant/widget.ts | Shadow DOM widget UI rendering and actions |
| apps/extension/src/content/form-assistant/index.ts | Content script orchestrator (Machine A) + applyValue plumbing |
| apps/extension/src/content/form-assistant/field-detector.ts | DOM → RawFieldInput → sanitized/classified FieldDescriptor |
| apps/extension/src/background/index.ts | SW handlers for status/enable/request (Phase 1 local-only) |
Comments suppressed due to low confidence (1)
apps/extension/src/lib/core/form-assistant/build-field-prompt.ts:48
- Même problème que pour email/téléphone : la consigne
""peut produire une sortie littérale avec guillemets, donc non-vide. Demander explicitement une chaîne vide sans guillemets évite une valeur invalide à insérer.
case 'linkedin':
return 'Réponds uniquement par une URL LinkedIn neutre si le profil en indique une, sinon une chaîne vide : ""';
- content/index.ts: fix FORM_ASSIST_STATUS response read — the SW returns
a bridge message { type, payload }, so read result.payload.enabled
(was result.enabled, leaving the assistant stuck disabled).
- content/index.ts: fix stale model path reference in doc comment
(formassistant -> form-assistant).
- core/build-field-prompt.ts: drop literal '""' from email/phone/linkedin
instructions to avoid the LLM returning the quotes verbatim; ask for an
empty string with no characters instead.
- dev/chrome-stubs.ts: return empty strings for email/phone/linkedin to
mirror production (profile carries no such fields; avoids fake PII in
dev UX validation).
- models/form-assistant.model.md: align placeholder cap doc with the
real implementation (<=200, not <=120; label stays <=120).
- content/widget.ts: fix typo 'Isoled' -> 'Isolé'.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5258eea314
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Widget & DOM:
- Click delegation via closest('[data-action]') (fixes nested span clicks)
- Right-edge viewport clamp in anchorTo (prevents overflow)
- Skip widget-internal focus events (isHostElement guard)
- Document mousedown listener for outside-click dismissal (shadow DOM safe)
- Multi-ID aria-labelledby resolution in field-detector
- Exclude readOnly/disabled/aria-readonly/aria-disabled fields
Orchestration (Machine A):
- applyValue returns boolean; failure shows error view instead of hiding
- Track activeRequestId; cancel in-flight SW request on field switch/dismiss
- Stale-response guard also checks activeRequestId match
Cancellation (model-mandated via AbortSignal):
- FORM_ASSIST_CANCEL message + FORM_ASSIST_CANCEL_ACK (Zod strict schemas)
- SW AbortController map; abort -> 'cancelled' error code
- local-generator uses promptWithCancellation/throwIfAborted/abortableDelay
(mirrors semantic-scorer)
Prompt & parsing:
- Include profile.availability in build-field-prompt
- Reject literal "" sentinel in parse-field-proposal
- Strip one level of surrounding quotes from LLM output
- Org/user marker guard in classify-field ("Nom de l'entreprise" -> free-text)
Performance:
- Bounded in-memory cache in local-generator (64 entries, FIFO)
Broadcast:
- FORM_ASSIST_ENABLED now also reaches content scripts via chrome.tabs.sendMessage
Tests: +11 cases (org marker guard, quote stripping, availability injection,
keyword cap, determinism). 56/56 form-assistant tests pass.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Summary
Adds a Grammarly-style form assistant that activates when the user focuses an eligible input on known freelance-mission platforms. It proposes field values derived from the local user profile via Gemini Nano (on-device, free) — no backend, no credentials sent. Phase 1 is local-only; the remote Eve/Vercel engine path is modeled and reserved for Phase 2.
This unblocks the "apply with AI assistance" need across postuler (application) flows without storing or transmitting credentials, staying consistent with the project's local-first architecture.
Motivation: users re-type the same profile data (name, title, keywords, TJM, availability...) into every platform's application form. This assistant surfaces the right value for the right field, on-device, with explicit accept-before-apply.
Approach
Strict Model → Review → Implement → Verify, honoring "the LLM produces signals; the model decides."
src/lib/core/form-assistant/:types,classify-field,sanitize-field-descriptor,select-engine,build-field-prompt,parse-field-proposal,redact-for-remote. Shell may import core; core never imports shell.src/lib/shell/form-assistant/: isolatedchrome.storage.localsettings key (formAssist, separate fromAppSettingsto avoid touching the settings-release migration contract system), andlocal-generator.ts(Gemini Nano session with 8s timeout +[500,1000ms]retry, matching themission-generatorpattern).src/content/form-assistant/:field-detector(DOM →FieldDescriptor),widget(closed Shadow DOM, 4 states: armed/requesting/ready/error, viewport-aware anchoring),index.tsorchestrator (focus detection, bridge messaging, native-setterapplyValuecompatible with React/Svelte synthetic events; ContentEditable viaexecCommand('insertText')).background/index.ts(FORM_ASSIST_STATUS,FORM_ASSIST_ENABLE,FORM_ASSIST_REQUEST) with lazy imports.FORM_ASSIST_*messages +FieldDescriptorSchemainschemas.ts, all Zod.strict().meta.tsgainsformAssist?: boolean(Free-Work pilot opt-in);vite.config.tsderivescontent_scripts.matchesfrom the connector catalog (no duplicate hand-maintained list);chrome-stubs.tsmocks the storage key + per-FieldKindproposals for dev mode.form-assistant.model.md(3 state machines A/B/C),form-assistant.review.md(nominal/error/cancel/permission/terminal matrix),openspec/changes/form-assistant/proposal.md.Safety: no auto-submission. Values are applied only after explicit user acceptance. Sensitive inputs (password, checkbox, radio, hidden, date, number, file...) are rejected at detection time.
Non-obvious points / trade-offs
chrome.storage.localkey rather thanAppSettings. This deliberately avoids touching the settings-release migration contract system (7+ files). Worth flagging if we later want a unified settings surface.formAssist: trueis set only on Free-Work inmeta.ts. Other connectors opt in by flipping that flag; the manifestcontent_scripts.matchesare auto-derived.select-engineandredact-for-remotealready model the remote (Eve) path and field redaction, but the SW handler only wires the local branch. Machine C (consent) and the Vercel endpoint are Phase 2.Verification
pnpm format:checkpnpm lint(0 errors; 8 pre-existing warnings inlocal-data-reset.contract.ts, unrelated)pnpm typecheckpnpm test— 3483 passed / 225 files. 45/45 new form-assistant tests green (mock-free Core).pnpm build— content-script chunk emitted; manifestcontent_scriptscorrectly deriveshttps://www.free-work.com/*.Checklist