diff --git a/extension/src/shared/form-affordances/__tests__/group-autocomplete.test.ts b/extension/src/shared/form-affordances/__tests__/group-autocomplete.test.ts
new file mode 100644
index 0000000..da6b0ed
--- /dev/null
+++ b/extension/src/shared/form-affordances/__tests__/group-autocomplete.test.ts
@@ -0,0 +1,35 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { wireGroupAutocomplete } from '../group-autocomplete';
+
+describe('wireGroupAutocomplete', () => {
+ let form: HTMLElement;
+
+ beforeEach(() => {
+ // Clean up any datalist from a prior test
+ document.getElementById('groups-datalist')?.remove();
+ form = document.createElement('div');
+ form.innerHTML = ``;
+ document.body.appendChild(form);
+ });
+
+ it('attaches datalist with all groups', async () => {
+ const sendMessage = vi.fn().mockResolvedValue({
+ ok: true,
+ data: { groups: ['personal', 'work', 'finance'] },
+ });
+ await wireGroupAutocomplete(form, { sendMessage });
+ const list = document.getElementById('groups-datalist') as HTMLDataListElement | null;
+ expect(list).not.toBeNull();
+ const opts = Array.from(list!.querySelectorAll('option')).map((o) => o.value);
+ expect(opts).toEqual(['personal', 'work', 'finance']);
+ const input = form.querySelector('#f-group') as HTMLInputElement;
+ expect(input.getAttribute('list')).toBe('groups-datalist');
+ });
+
+ it('is a no-op if SW returns error', async () => {
+ const sendMessage = vi.fn().mockResolvedValue({ ok: false, error: 'vault_locked' });
+ await wireGroupAutocomplete(form, { sendMessage });
+ const input = form.querySelector('#f-group') as HTMLInputElement;
+ expect(input.getAttribute('list')).toBeNull();
+ });
+});
diff --git a/extension/src/shared/form-affordances/group-autocomplete.ts b/extension/src/shared/form-affordances/group-autocomplete.ts
new file mode 100644
index 0000000..60a4c89
--- /dev/null
+++ b/extension/src/shared/form-affordances/group-autocomplete.ts
@@ -0,0 +1,23 @@
+export interface GroupAutocompleteOpts {
+ sendMessage: (msg: { type: 'list_groups' }) => Promise<{ ok: boolean; data?: { groups: string[] }; error?: string }>;
+}
+
+const DATALIST_ID = 'groups-datalist';
+
+export async function wireGroupAutocomplete(form: HTMLElement, opts: GroupAutocompleteOpts): Promise {
+ const input = form.querySelector('#f-group');
+ if (!input) return;
+ const resp = await opts.sendMessage({ type: 'list_groups' });
+ if (!resp.ok || !resp.data) return;
+
+ // Datalists must live in the document, not nested inside an input. Reuse if
+ // we've already mounted one this session.
+ let list = document.getElementById(DATALIST_ID) as HTMLDataListElement | null;
+ if (!list) {
+ list = document.createElement('datalist');
+ list.id = DATALIST_ID;
+ document.body.appendChild(list);
+ }
+ list.innerHTML = resp.data.groups.map((g) => ``).join('');
+ input.setAttribute('list', DATALIST_ID);
+}