Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/extension/src/lib/shell/connectors/meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ const CATALOG: readonly ConnectorMeta[] = [
icon: 'https://www.google.com/s2/favicons?domain=free-work.com&sz=32',
url: 'https://www.free-work.com',
hostPermissions: ['https://www.free-work.com/*'],
// Pilote Form Assistant Phase 1 (Gemini Nano local uniquement).
formAssist: true,
},
{
Expand All @@ -41,6 +40,7 @@ const CATALOG: readonly ConnectorMeta[] = [
icon: 'https://www.google.com/s2/favicons?domain=lehibou.com&sz=32',
url: 'https://www.lehibou.com',
hostPermissions: ['https://*.lehibou.com/*'],
formAssist: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restrict activation to application fields

For every newly opted-in connector, this flag injects the assistant across the connector's entire host pattern, but content/form-assistant/field-detector.ts:110-132 accepts every writable text-like input and the classifier maps unmatched or unlabeled fields to free-text. The widget will therefore arm on login email boxes, global search bars, chat inputs, and other non-application fields throughout LeHibou and the other newly enabled sites, contrary to the application-form scope described by form-assistant.model.md. Add an application-context or eligible-field guard before enabling these whole origins.

Useful? React with 👍 / 👎.

},
{
id: 'hiway',
Expand All @@ -50,27 +50,31 @@ const CATALOG: readonly ConnectorMeta[] = [
// Hiway fetches missions from a Supabase REST endpoint; that host is
// Hiway-owned infra and must be dropped when Hiway is excluded.
hostPermissions: ['https://hiway-missions.fr/*', 'https://jhgjtlkfewuiiofxfrvh.supabase.co/*'],
formAssist: true,
},
{
id: 'collective',
name: 'Collective',
icon: 'https://www.google.com/s2/favicons?domain=collective.work&sz=32',
url: 'https://app.collective.work/',
hostPermissions: ['https://*.collective.work/*'],
formAssist: true,
},
{
id: 'cherry-pick',
name: 'Cherry Pick',
icon: 'https://www.google.com/s2/favicons?domain=cherry-pick.io&sz=32',
url: 'https://www.cherry-pick.io',
hostPermissions: ['https://app.cherry-pick.io/*'],
formAssist: true,
},
{
id: 'malt',
name: 'Malt',
icon: 'https://www.google.com/s2/favicons?domain=malt.fr&sz=32',
url: 'https://www.malt.fr',
hostPermissions: ['https://*.malt.fr/*', 'https://*.malt.io/*'],
formAssist: true,
},
] as const;

Expand Down
73 changes: 73 additions & 0 deletions apps/extension/src/lib/state/settings-page.svelte.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class SettingsPageController {
private readonly shippedConnectorCatalog: readonly ConnectorMeta[];
private readonly resetAvailability: LocalDataResetRuntimeAvailability;
private readonly unsubscribeProfileMessages = this.subscribeProfileMessages();
private readonly unsubscribeFormAssistMessages = this.subscribeFormAssistMessages();
private readonly unsubscribeSettingsSnapshots: () => void;

private readonly profileActor = createProfileStore({
Expand Down Expand Up @@ -127,6 +128,14 @@ export class SettingsPageController {
connectedPendingDownloads = $state(0);
connectedSyncError = $state<string | null>(null);

/**
* Form Assistant activation (Machine D — src/models/form-assistant.model.md).
* Miroir de l'état persisté dans le SW (chrome.storage.local). Le toggle ne
* écrit JAMAIS directement en storage : il émet FORM_ASSIST_ENABLE via bridge.
*/
formAssistEnabled = $state(false);
formAssistStatus = $state<'loading' | 'ready' | 'error'>('loading');

showResetConfirm = $state(false);
resetError = $state<string | null>(null);

Expand Down Expand Up @@ -173,8 +182,26 @@ export class SettingsPageController {
}
}

/**
* Racolement Machine D : le SW diffuse FORM_ASSIST_ENABLED après toute
* mutation persistée. Le panel est un miroir, jamais la source primaire.
*/
private subscribeFormAssistMessages(): () => void {
try {
return subscribeMessages((message) => {
if (message.type === 'FORM_ASSIST_ENABLED') {
this.formAssistEnabled = Boolean(message.payload.enabled);
this.formAssistStatus = 'ready';
}
});
} catch {
return () => {};
}
}

destroy(): void {
this.unsubscribeProfileMessages();
this.unsubscribeFormAssistMessages();
this.unsubscribeSettingsSnapshots();
}

Expand All @@ -201,6 +228,7 @@ export class SettingsPageController {
this.loadSettings(),
this.loadConnectedAccount(),
this.loadScanHistory(),
this.loadFormAssist(),
]);
}

Expand Down Expand Up @@ -237,6 +265,51 @@ export class SettingsPageController {
}
}

/**
* Lit l'état persisté du Form Assistant auprès du SW (Machine D — INIT).
* Échec (SW injoignable) → état `error` mais la page reste utilisable.
*/
async loadFormAssist(): Promise<void> {
this.formAssistStatus = 'loading';
try {
const result = (await sendMessage({ type: 'FORM_ASSIST_STATUS' })) as
{ type: 'FORM_ASSIST_STATUS_RESULT'; payload: { enabled: boolean } } | undefined;
this.formAssistEnabled = Boolean(result?.payload.enabled);
this.formAssistStatus = 'ready';
} catch {
this.formAssistStatus = 'error';
}
}
Comment on lines +272 to +282

/**
* Bascule l'activation du Form Assistant. Le SW persiste et diffuse
* FORM_ASSIST_ENABLED (racolement). L'UI reste optimiste : on met à jour
* immédiatement pour la réactivité, et le message SW confirme/rétablit.
*/
async toggleFormAssist(): Promise<void> {
if (this.formAssistStatus === 'loading') {
return;
}
const next = !this.formAssistEnabled;
const previous = this.formAssistEnabled;
this.formAssistEnabled = next;
this.formAssistStatus = 'loading';
try {
const result = (await sendMessage({
type: 'FORM_ASSIST_ENABLE',
payload: { enabled: next },
})) as { type: 'FORM_ASSIST_ENABLED'; payload: { enabled: boolean } } | undefined;
// La réponse du SW est la source de vérité (elle peut différer de
// l'optimisme en cas d'erreur persistée côté SW).
this.formAssistEnabled = Boolean(result?.payload.enabled);
this.formAssistStatus = 'ready';
Comment on lines +304 to +305

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve enabled state when disabling fails

When a user disables an enabled assistant and chrome.storage.local.set fails, the existing SW handler in background/index.ts:2298-2303 catches the error and resolves with FORM_ASSIST_ENABLED { enabled: false } instead of rejecting. This branch therefore marks the toggle off/ready and skips the restore/toast, while persisted enabled: true remains and already-armed content scripts receive no disable broadcast. Have the shell return a typed failure and only project the confirmed state on actual persistence success.

AGENTS.md reference: AGENTS.md:L89-L94

Useful? React with 👍 / 👎.

} catch {
this.formAssistEnabled = previous;
this.formAssistStatus = 'error';
await showToast("Impossible d'activer l'assistant de candidature", 'error');
}
}
Comment on lines +289 to +311

async loadSettings(): Promise<void> {
try {
const settings = await getSettings();
Expand Down
44 changes: 42 additions & 2 deletions apps/extension/src/models/form-assistant.model.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,37 @@ interface FieldDescriptor {
// JAMAIS : id/name du champ, valeurs d'autres champs, URL, HTML voisin.
```

## Machine D — Activation côté panel (side panel ↔ SW)

Le toggle d'activation vit dans la page **Settings**. Il reflète l'état persisté
dans le SW (`formAssist.enabled`, `chrome.storage.local`) et émet
`FORM_ASSIST_ENABLE` sur action utilisateur explicite. **Aucun LLM dans cette
machine** : la décision est purement un booléen user-owned persisté côté SW.

```text
unknown ──INIT──► loading
loading ──STATUS_RESULT(enabled)──► on | off
loading ──SW_UNREACHABLE──► error
off ──TOGGLE(on)──► loading (optimiste, garde anti-double-clic)
on ──TOGGLE(off)──► loading
on|off ──ENABLED(enabled)──► on | off (raccolement via diffusion SW)
error ──RETRY──► loading
```

- `loading` après `TOGGLE` est **optimiste** : l'UI désactive le toggle
(`disabled`) jusqu'au `FORM_ASSIST_ENABLED` de confirmation pour interdire
tout double-envoi. L'échec (`SW_UNREACHABLE`) ramène à l'état précédent **et**
affiche un toast typé (jamais silencieux).
- Le `FORM_ASSIST_ENABLED` diffusé par le SW (qui notifie aussi le content
- script) racolette le toggle : le panel est une **source de vérité miroir**, pas
primaire. La source primaire reste le `chrome.storage.local` du SW.
Comment on lines +125 to +127
- Le moteur distant (Eve) reste **hors périmètre du toggle** : ce dernier
n'expose que le booléen `enabled`. La préférence `engine` et le consentement
(Machine C) sont gérés séparément.
- Invariant : le toggle **n'écrit jamais** dans `chrome.storage` depuis le
panel — tout passe par le bridge typé vers le SW (règle « le panel n'appelle
jamais IndexedDB / chrome.storage directement »).

## Machine A — Widget (content script, par champ focalisé)

```text
Expand Down Expand Up @@ -171,8 +202,12 @@ denied ─PROMPT───► prompting

## Messages bridge (nouveaux)

- `FORM_ASSIST_ENABLE` (panel → SW) — `{ enabled, enginePref, perSite? }`.
- `FORM_ASSIST_STATUS` (SW → panel/content) — `{ armed, engineAvailability }`.
- `FORM_ASSIST_ENABLE` (panel → SW) — `{ enabled }` (booléen user-owned ;
`enginePref`/`perSite` restent HORS périmètre du toggle v1, gérés par défaut).
Le SW persiste, puis **diffuse** `FORM_ASSIST_ENABLED` vers le panel (racolement)
ET vers le content script (arm/disarm).
- `FORM_ASSIST_STATUS` (panel/content → SW) — lecture de l'état persisté.
- `FORM_ASSIST_STATUS_RESULT` (SW → panel/content) — `{ enabled, engine }`.
- `FORM_ASSIST_REQUEST` (content → SW) — `{ requestId, field: FieldDescriptor }`
(le SW relit le profil canonique lui-même ; **pas de profil côté contenu**).
- `FORM_ASSIST_PROPOSAL` (SW → content) — `{ requestId, proposal: { text, engine } }`.
Expand Down Expand Up @@ -206,6 +241,11 @@ rollout (même exigence que le Copilot dossier).
qui déclarent `formAssist: true` dans le catalogue (`meta.ts`). Un connecteur
exclu au build (cf. `connector-build-config.model.md`) exclut aussi
l'assistant — cohérence de moindre privilège.
- **Tous les connecteurs du catalogue** déclarent `formAssist: true` par défaut
(Free-Work, LeHibou, Hiway, Collective, Cherry Pick, Malt) — conformément à
la portée produit ci-dessus. Le content script étant **générique**
(détection de champ conservatrice, aucun parsing spécifique à une plateforme),
l'activation ne dépend que du flag catalogue, pas d'une logique par site.
- Aucune nouvelle permission large : on réutilise `host_permissions` +
`scripting` + `storage`.
- En **dev**, le content script se charge mais les `chrome.*` sont stubés
Expand Down
38 changes: 38 additions & 0 deletions apps/extension/src/ui/pages/SettingsPage.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,44 @@
</div>
</div>
</div>

<!-- Form Assistant (Machine D — src/models/form-assistant.model.md) -->
<div class="section-card rounded-xl p-5 space-y-3">
<div class="flex items-start gap-3">
<div
class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-blueprint-blue/6"
>
<Icon name="file-text" size={14} class="text-blueprint-blue" />
</div>
<div class="min-w-0 flex-1">
<h3 class="text-body-lg font-medium text-text-primary">Assistant de candidature</h3>
<p class="mt-1 text-meta text-text-subtle">
Au focus sur un champ d'un formulaire de candidature, propose une valeur issue de
votre profil. Vous restez seul à valider l'insertion (rien n'est jamais rempli
automatiquement).
</p>
</div>
<Toggle
checked={settings.formAssistEnabled}
disabled={settings.formAssistStatus === 'loading'}
aria-label="Activer l'assistant de candidature"
onclick={() => settings.toggleFormAssist()}
/>
</div>
<div class="rounded-lg border border-blueprint-blue/15 bg-blueprint-blue/5 px-3 py-3">
<div class="flex items-start gap-2">
<Icon name="shield-check" size={14} class="mt-0.5 shrink-0 text-blueprint-blue" />
<div class="min-w-0">
<p class="text-meta font-medium text-text-primary">Périmètre et confidentialité</p>
<p class="mt-1 text-caption leading-5 text-text-subtle">
Actif uniquement sur les plateformes connecteurs compatibles (Free-Work pour la
phase pilote). La génération utilise Gemini Nano en local ; aucune donnée de page
Comment on lines +945 to +946

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the disclosed connector scope

The five non-Free-Work catalog entries are now marked formAssist: true, and vite.config.ts:83-92 converts every such connector's host permissions into content-script matches. The assistant is consequently shipped on LeHibou, Hiway, Collective, Cherry Pick, and Malt, while this disclosure still tells users that the pilot is limited to Free-Work. Update or derive this text so users understand where enabling the assistant makes it run.

Useful? React with 👍 / 👎.

n'est envoyée à un serveur.
Comment on lines +945 to +947
</p>
</div>
</div>
</div>
</div>
</section>

<section id="settings-data" class="scroll-mt-4 space-y-4" aria-labelledby="settings-data-title">
Expand Down
Loading
Loading