Skip to content

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 (add host_permissions: ["http://192.168.86.50:7365/*"], temporarily set action.default_popup: "spike.html"), chrome-extension-forge-chat/spike.html, chrome-extension-forge-chat/spike.js (fetch http://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). A TypeError/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) or BLOCKED: <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 of sites/justinsforge.com/chat/index.html, minus the external Google Fonts <link>s, minus the JustinsForge.com home link, with <script>window.FORGE_LAUNCHER_API_BASE='http://192.168.86.50:7365/api'</script> before the launcher script, and local launcher.css/launcher.js refs 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 (remove action.default_popup/spike, add "side_panel": {"default_path": "sidepanel.html"}, add "sidePanel" to permissions, keep host_permissions, bump version to 1.2.0), chrome-extension-forge-chat/background.js (replace window-create logic with chrome.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.js to vector:forge-chat-ext/, delete spike.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 (set FORGE_LAUNCHER_API_BASE='https://chat.justinsforge.com/api'), chrome-extension-forge-chat/background.js or a small cf-headers.js (attach CF-Access-Client-Id/CF-Access-Client-Secret via declarativeNetRequest or 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.com Access 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/sessions returns 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_permissions https://chat.justinsforge.com/*, declarativeNetRequest permission + 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 with URL: line), MEMORY.md index 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/ | head shows the files tracked; bash /home/justinwieb/forge/scripts/forge_eval_run.sh passes if applicable.
  • Commit: docs(forge-chat): track extension + reference topic + MEMORY index