diff --git a/extension/src/popup/styles.css b/extension/src/popup/styles.css
index 6a35990..5e5dcb6 100644
--- a/extension/src/popup/styles.css
+++ b/extension/src/popup/styles.css
@@ -1362,3 +1362,26 @@ textarea {
opacity: 0.4;
cursor: not-allowed;
}
+
+.hostname-chip-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 4px;
+ font-size: 11px;
+ color: var(--text-muted);
+}
+.hostname-chip {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ border-radius: 3px;
+ font-size: 10px;
+ font-weight: 600;
+ color: #0c1118;
+}
+.hostname-text {
+ font-family: ui-monospace, monospace;
+}
diff --git a/extension/src/shared/form-affordances/__tests__/url-tools.test.ts b/extension/src/shared/form-affordances/__tests__/url-tools.test.ts
index 7794f84..0373ca8 100644
--- a/extension/src/shared/form-affordances/__tests__/url-tools.test.ts
+++ b/extension/src/shared/form-affordances/__tests__/url-tools.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { wireFillFromTab } from '../url-tools';
+import { wireFillFromTab, wireHostnameChip } from '../url-tools';
describe('wireFillFromTab', () => {
let form: HTMLElement;
@@ -44,3 +44,60 @@ describe('wireFillFromTab', () => {
expect((form.querySelector('#fill-from-tab-btn') as HTMLButtonElement).disabled).toBe(true);
});
});
+
+describe('wireHostnameChip', () => {
+ let form: HTMLElement;
+
+ beforeEach(() => {
+ form = document.createElement('div');
+ form.innerHTML = `
+
+ `;
+ document.body.appendChild(form);
+ vi.useFakeTimers();
+ });
+
+ it('renders chip + hostname on valid URL after debounce', () => {
+ wireHostnameChip(form);
+ const input = form.querySelector('#f-url') as HTMLInputElement;
+ input.value = 'https://github.com/login';
+ input.dispatchEvent(new Event('input'));
+ vi.advanceTimersByTime(250);
+ const row = form.querySelector('#hostname-chip-row') as HTMLElement;
+ expect(row.hidden).toBe(false);
+ expect(row.textContent).toContain('github.com');
+ expect(row.querySelector('.hostname-chip')?.textContent).toBe('G');
+ });
+
+ it('hides chip if URL is empty', () => {
+ wireHostnameChip(form);
+ const input = form.querySelector('#f-url') as HTMLInputElement;
+ input.value = '';
+ input.dispatchEvent(new Event('input'));
+ vi.advanceTimersByTime(250);
+ expect((form.querySelector('#hostname-chip-row') as HTMLElement).hidden).toBe(true);
+ });
+
+ it('hides chip if URL does not parse', () => {
+ wireHostnameChip(form);
+ const input = form.querySelector('#f-url') as HTMLInputElement;
+ input.value = '!!!not-a-url';
+ input.dispatchEvent(new Event('input'));
+ vi.advanceTimersByTime(250);
+ expect((form.querySelector('#hostname-chip-row') as HTMLElement).hidden).toBe(true);
+ });
+
+ it('treats scheme-less host as https://', () => {
+ wireHostnameChip(form);
+ const input = form.querySelector('#f-url') as HTMLInputElement;
+ input.value = 'gitlab.com/users/sign_in';
+ input.dispatchEvent(new Event('input'));
+ vi.advanceTimersByTime(250);
+ const row = form.querySelector('#hostname-chip-row') as HTMLElement;
+ expect(row.hidden).toBe(false);
+ expect(row.textContent).toContain('gitlab.com');
+ });
+});
diff --git a/extension/src/shared/form-affordances/url-tools.ts b/extension/src/shared/form-affordances/url-tools.ts
index 5f71646..5b6b0ae 100644
--- a/extension/src/shared/form-affordances/url-tools.ts
+++ b/extension/src/shared/form-affordances/url-tools.ts
@@ -22,3 +22,58 @@ export function wireFillFromTab(form: HTMLElement, opts: FillFromTabOpts): void
}
export const FILL_FROM_TAB_BTN_HTML = ``;
+
+const CHIP_HUES = [
+ '#5ea0c4', '#c47e5e', '#5ec47a', '#c45e9c',
+ '#a3c45e', '#7e5ec4', '#c4b75e', '#5ec4c4',
+];
+
+function hostnameHue(host: string): string {
+ let h = 0;
+ for (let i = 0; i < host.length; i++) h = (h * 31 + host.charCodeAt(i)) | 0;
+ return CHIP_HUES[Math.abs(h) % CHIP_HUES.length];
+}
+
+function tryParseHost(raw: string): string | null {
+ const trimmed = raw.trim();
+ if (!trimmed) return null;
+ const candidate = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\//.test(trimmed) ? trimmed : `https://${trimmed}`;
+ try {
+ const u = new URL(candidate);
+ const host = u.host || null;
+ if (!host) return null;
+ // Validate hostname contains only valid characters
+ if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/.test(host)) {
+ return null;
+ }
+ return host;
+ } catch {
+ return null;
+ }
+}
+
+export function wireHostnameChip(form: HTMLElement): void {
+ const input = form.querySelector('#f-url');
+ const row = form.querySelector('#hostname-chip-row');
+ if (!input || !row) return;
+ let timer: ReturnType | null = null;
+
+ const update = () => {
+ const host = tryParseHost(input.value);
+ if (!host) {
+ row.hidden = true;
+ row.innerHTML = '';
+ return;
+ }
+ const initial = host[0]?.toUpperCase() ?? '?';
+ const hue = hostnameHue(host);
+ row.hidden = false;
+ row.innerHTML = `${initial}${host}`;
+ };
+
+ input.addEventListener('input', () => {
+ if (timer !== null) clearTimeout(timer);
+ timer = setTimeout(() => { timer = null; update(); }, 200);
+ });
+ update(); // initial render for prefilled values
+}
diff --git a/extension/src/vault/vault.css b/extension/src/vault/vault.css
index 02763eb..bb808e8 100644
--- a/extension/src/vault/vault.css
+++ b/extension/src/vault/vault.css
@@ -1392,3 +1392,26 @@ textarea {
opacity: 0.4;
cursor: not-allowed;
}
+
+.hostname-chip-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ margin-top: 4px;
+ font-size: 11px;
+ color: var(--text-muted);
+}
+.hostname-chip {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ border-radius: 3px;
+ font-size: 10px;
+ font-weight: 600;
+ color: #0c1118;
+}
+.hostname-text {
+ font-family: ui-monospace, monospace;
+}