Plan: Forge Chat Chrome extension as a docked side panel¶
URL: https://mkdocs.justinsforge.com/memory/plans/forge-chat-side-panel-2026-07-02/
Date: 2026-07-02
Approved spec: Rebuild the Forge Chat extension so the toolbar icon opens the session launcher as a Chrome side panel docked to the right edge (resizes/moves with the window, stays open on click-away) instead of a floating window. The launcher app (sites/justinsforge.com/chat/) is a self-contained vanilla JS SPA that already reads window.FORGE_LAUNCHER_API_BASE and authenticates via Authorization: Bearer <token>, so this is a packaging job. Transport is direct LAN to Console http://192.168.86.50:7365/api (proven reachable + auth-gated from Vector; Tailscale blocked by ACL, CF not needed). Token pasted once into the app's built-in field, never committed.
Out of scope:
- Sol (Mac) deployment: do Vector first, replicate after it works.
- Bundling the Outfit web font: drop the Google Fonts link, use system fallback.
- Any server-side change to forge_session_launcher_api.py unless the spike forces CORS.
- Changing the launcher app's own source in sites/ (we copy, not fork behavior).
Open risk gating the build¶
chrome-extension pages are secure contexts; a fetch() to plaintext http://192.168.86.50 may be blocked by Chrome mixed-content / Private Network Access policy. Task 1 is a spike that answers this crisply before any bundling. If blocked, switch transport to a Cloudflare Access service token over https://chat.justinsforge.com (Tasks 5a/5b) instead of LAN http.
Task list¶
Task 1: Spike — can an extension page fetch the LAN API?¶
- Files:
chrome-extension-forge-chat/manifest.json(addhost_permissions: ["http://192.168.86.50:7365/*"], temporarily setaction.default_popup: "spike.html"),chrome-extension-forge-chat/spike.html,chrome-extension-forge-chat/spike.js(fetchhttp://192.168.86.50:7365/api/sessions, render the HTTP status or the thrown error text large on the page). - What: Minimal popup page whose only job is to prove reachability from an extension context.
403 invalid token= reachable, mixed content NOT blocked (win). ATypeError/blocked message = mixed content blocked (pivot to CF token). - Verification: scp to Vector, Justin reloads the card and clicks the icon; page shows either
REACHABLE: HTTP 403(proceed to Task 2) orBLOCKED: <error>(proceed to Task 5a). - Commit:
spike(forge-chat): extension-context LAN fetch reachability probe
Task 2: Bundle the launcher as the side-panel page¶
- Files:
chrome-extension-forge-chat/sidepanel.html(copy ofsites/justinsforge.com/chat/index.html, minus the external Google Fonts<link>s, minus theJustinsForge.comhome link, with<script>window.FORGE_LAUNCHER_API_BASE='http://192.168.86.50:7365/api'</script>before the launcher script, and locallauncher.css/launcher.jsrefs without?v=query strings),chrome-extension-forge-chat/launcher.css(copied),chrome-extension-forge-chat/launcher.js(copied). - What: Self-contained side-panel document that loads the existing SPA pointed at the LAN API.
- Verification:
python3 -c "import pathlib,re; h=pathlib.read_text if False else open('/home/justinwieb/forge/chrome-extension-forge-chat/sidepanel.html').read(); assert 'FORGE_LAUNCHER_API_BASE' in h and 'fonts.googleapis' not in h and '?v=' not in h; print('sidepanel wired OK')" - Commit:
feat(forge-chat): bundle launcher SPA as sidepanel.html pointed at LAN API
Task 3: Switch the extension from window to side panel¶
- Files:
chrome-extension-forge-chat/manifest.json(removeaction.default_popup/spike, add"side_panel": {"default_path": "sidepanel.html"}, add"sidePanel"topermissions, keephost_permissions, bumpversionto1.2.0),chrome-extension-forge-chat/background.js(replace window-create logic withchrome.sidePanel.setPanelBehavior({openPanelOnActionClick:true})on install, or open panel in the action click handler). - What: Toolbar click now opens the docked side panel instead of a window.
- Verification:
node --check /home/justinwieb/forge/chrome-extension-forge-chat/background.js && python3 -c "import json;m=json.load(open('/home/justinwieb/forge/chrome-extension-forge-chat/manifest.json'));assert m['side_panel']['default_path']=='sidepanel.html' and 'sidePanel' in m['permissions'];print('manifest side-panel OK', m['version'])" - Commit:
feat(forge-chat): open as docked side panel instead of window (v1.2.0)
Task 4: Deploy to Vector and verify end-to-end¶
- Files: none (deploy only): scp
manifest.json background.js sidepanel.html launcher.css launcher.jstovector:forge-chat-ext/, deletespike.html spike.js popup.* background-window remnants there. - What: Push the side-panel build to the live Vector copy and confirm it works with a real token.
- Verification: Justin on Vector reloads the card, clicks the icon, pastes the bearer token once; the side panel docks and lists sessions. Confirm it stays open when clicking into the page and resizes with the window.
- Commit:
chore(forge-chat): deploy side-panel build to Vector(no-op if nothing tracked changed; otherwise doc note)
Task 5a (conditional — only if Task 1 = BLOCKED): CF Access service token transport¶
- Files:
chrome-extension-forge-chat/sidepanel.html(setFORGE_LAUNCHER_API_BASE='https://chat.justinsforge.com/api'),chrome-extension-forge-chat/background.jsor a smallcf-headers.js(attachCF-Access-Client-Id/CF-Access-Client-SecretviadeclarativeNetRequestor fetch wrapper),~/.forge-secrets/forge-chat-cf-token.env(client id/secret, chmod 600, NOT in repo). - What: Create a CF Access service token, add it to the
chat.justinsforge.comAccess policy, have the extension send the service-token headers so requests pass CF over https (no mixed content). - Verification:
curl -fsS -H "CF-Access-Client-Id: $ID" -H "CF-Access-Client-Secret: $SECRET" https://chat.justinsforge.com/api/sessionsreturns JSON (from Vector). - Commit:
feat(forge-chat): CF Access service-token transport for side panel
Task 5b (conditional): host_permissions + declarativeNetRequest for CF headers¶
- Files:
chrome-extension-forge-chat/manifest.json(host_permissionshttps://chat.justinsforge.com/*,declarativeNetRequestpermission + rules file), rules json. - What: Inject the CF service-token headers on outbound requests without hardcoding secrets in page JS.
- Verification: side panel lists sessions on Vector with no token-in-page.
- Commit:
feat(forge-chat): DNR header injection for CF service token
Task 6: Register and document¶
- Files:
chrome-extension-forge-chat/README.md(rewrite for side-panel mode + transport),memory/general/reference_forge_chat_extension.md(new topic file withURL:line),MEMORY.mdindex entry,git add chrome-extension-forge-chat/(folder currently untracked). - What: Track the extension in git, document the design + transport + token setup so Sol install is a
git pull. - Verification:
git ls-files chrome-extension-forge-chat/ | headshows the files tracked;bash /home/justinwieb/forge/scripts/forge_eval_run.shpasses if applicable. - Commit:
docs(forge-chat): track extension + reference topic + MEMORY index