refactor(design): board JS uses relative paths; drop __GSTACK_SERVER_URL injection

Board JS in design/src/compare.ts now calls ./api/feedback and ./api/progress
(relative to location.pathname) and feature-detects server mode via
location.protocol instead of the injected window.__GSTACK_SERVER_URL global.
The injection in design/src/serve.ts is removed (dead code now that nothing
reads it). Tests updated to match the new contract: serve.test.ts asserts
the relative-path JS is present and the global is gone; feedback-roundtrip
asserts location.protocol detects HTTP mode.

Why: prep for the multi-board daemon (design/src/daemon.ts upcoming) where
the same generated HTML is served at /boards/<id>/ instead of /. Relative
paths resolve against location.pathname in both cases, so one HTML, two
hosts. The injection was the only thing tying board JS to a specific
serving path; removing it unblocks the daemon work without forking the
generator.

file:// fallback preserved via the location.protocol feature-detect — board
opened directly as a file still falls through to the DOM-only success path.

The 6 feedback-roundtrip browser tests continue to fail with
session.clearLoadedHtml undefined; that failure pre-exists this branch
(verified against HEAD with these edits stashed) and lives in
browse/src/write-commands.ts, not in the design code path. Tracking
separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-05-25 14:31:22 -07:00
parent 64f9aafa1e
commit f55595d594
4 changed files with 52 additions and 38 deletions
+17 -6
View File
@@ -391,6 +391,17 @@ export function generateCompareHtml(images: string[]): string {
<div id="feedback-result"></div>
<script>
// Feature-detect: are we being served over HTTP (by serve.ts or the
// daemon), or opened directly as a file:// URL? In file:// mode the
// board JS falls through to a DOM-only success path with no server
// round-trips. Using location.protocol instead of an injected global
// means the same generated HTML works at both / (legacy --no-daemon)
// and /boards/<id>/ (daemon) — relative URLs resolve against
// location.pathname in both cases.
function hasServer() {
return location.protocol === 'http:' || location.protocol === 'https:';
}
// View toggle
document.querySelectorAll('.view-toggle button').forEach(function(btn) {
btn.addEventListener('click', function() {
@@ -465,8 +476,8 @@ export function generateCompareHtml(images: string[]): string {
});
function postFeedback(feedback) {
if (!window.__GSTACK_SERVER_URL) return Promise.resolve(null);
return fetch(window.__GSTACK_SERVER_URL + '/api/feedback', {
if (!hasServer()) return Promise.resolve(null);
return fetch('./api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(feedback),
@@ -509,7 +520,7 @@ export function generateCompareHtml(images: string[]): string {
}
function startProgressPolling() {
if (!window.__GSTACK_SERVER_URL) return;
if (!hasServer()) return;
var pollCount = 0;
var maxPolls = 150; // 5 min at 2s intervals
var pollInterval = setInterval(function() {
@@ -523,7 +534,7 @@ export function generateCompareHtml(images: string[]): string {
'</div>';
return;
}
fetch(window.__GSTACK_SERVER_URL + '/api/progress')
fetch('./api/progress')
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.status === 'serving') {
@@ -563,7 +574,7 @@ export function generateCompareHtml(images: string[]): string {
postFeedback(feedback).then(function(result) {
if (result && result.received) {
showRegeneratingState();
} else if (window.__GSTACK_SERVER_URL) {
} else if (hasServer()) {
showPostFailure(feedback);
}
});
@@ -578,7 +589,7 @@ export function generateCompareHtml(images: string[]): string {
postFeedback(feedback).then(function(result) {
if (result && result.received) {
showPostSubmitState();
} else if (window.__GSTACK_SERVER_URL) {
} else if (hasServer()) {
showPostFailure(feedback);
} else {
// DOM-only mode (legacy / test)
+16 -13
View File
@@ -1,12 +1,18 @@
/**
* HTTP server for the design comparison board feedback loop.
*
* Replaces the broken file:// + DOM polling approach. The server:
* 1. Serves the comparison board HTML over HTTP
* 2. Injects __GSTACK_SERVER_URL so the board POSTs feedback here
* 3. Prints feedback JSON to stdout (agent reads it)
* 4. Stays alive across regeneration rounds (stateful)
* 5. Auto-opens in the user's default browser
* Legacy single-process path: spawned by `$D compare --serve --no-daemon`.
* The daemon (`design/src/daemon.ts`) handles default invocations and hosts
* multiple boards under `/boards/<id>/`; this file stays as the escape hatch
* for tests and debugging. Board JS uses relative URLs and a
* location.protocol feature-detect, so the same generated HTML works at
* both `/` (here) and `/boards/<id>/` (daemon).
*
* The server:
* 1. Serves the comparison board HTML over HTTP at `/`
* 2. Prints feedback JSON to stdout (agent reads it)
* 3. Stays alive across regeneration rounds (stateful)
* 4. Auto-opens in the user's default browser
*
* State machine:
*
@@ -69,17 +75,14 @@ export async function serve(options: ServeOptions): Promise<void> {
fetch(req) {
const url = new URL(req.url);
// Serve the comparison board HTML
// Serve the comparison board HTML. The board JS uses relative paths
// (./api/feedback, ./api/progress) and a location.protocol
// feature-detect, so no per-request injection is needed.
if (
req.method === "GET" &&
(url.pathname === "/" || url.pathname === "/index.html")
) {
// Inject the server URL so the board can POST feedback
const injected = htmlContent.replace(
"</head>",
`<script>window.__GSTACK_SERVER_URL = ${JSON.stringify(url.origin)};</script>\n</head>`,
);
return new Response(injected, {
return new Response(htmlContent, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}