refactor: server-side frame wrapping and helper.js consolidation

Move toggleSelect/send/selectedChoice from frame-template.html inline
script to helper.js so they're auto-injected. Server now detects bare
HTML fragments (no DOCTYPE/html tag) and wraps them in the frame
template automatically. Full documents pass through as before.

Fix dark mode in sendToClaude confirmation (was using hardcoded colors).
Fix test env var bug (BRAINSTORM_SCREEN -> BRAINSTORM_DIR).
Add tests for fragment wrapping, full doc passthrough, and helper.js.
This commit is contained in:
Jesse Vincent
2026-02-06 17:59:43 -08:00
parent 7398af9947
commit 5b00c6eb50
4 changed files with 137 additions and 69 deletions

View File

@@ -5,15 +5,32 @@ const chokidar = require('chokidar');
const fs = require('fs');
const path = require('path');
// Use provided port or pick a random high port (49152-65535)
const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
// Ensure screen directory exists
if (!fs.existsSync(SCREEN_DIR)) {
fs.mkdirSync(SCREEN_DIR, { recursive: true });
}
// Load frame template and helper script once at startup
const frameTemplate = fs.readFileSync(path.join(__dirname, 'frame-template.html'), 'utf-8');
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
const helperInjection = `<script>\n${helperScript}\n</script>`;
// Detect whether content is a full HTML document or a bare fragment
function isFullDocument(html) {
const trimmed = html.trimStart().toLowerCase();
return trimmed.startsWith('<!doctype') || trimmed.startsWith('<html');
}
// Wrap a content fragment in the frame template
function wrapInFrame(content) {
return frameTemplate.replace(
/(<div id="claude-content">)[\s\S]*?(<\/div>\s*<\/div>\s*<div class="feedback-footer">)/,
`$1\n ${content}\n $2`
);
}
// Find the newest .html file in the directory by mtime
function getNewestScreen() {
const files = fs.readdirSync(SCREEN_DIR)
@@ -28,7 +45,6 @@ function getNewestScreen() {
return files.length > 0 ? files[0].path : null;
}
// Default waiting page (served when no screens exist yet)
const WAITING_PAGE = `<!DOCTYPE html>
<html>
<head>
@@ -49,7 +65,6 @@ const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });
// Track connected browsers for reload notifications
const clients = new Set();
wss.on('connection', (ws) => {
@@ -57,7 +72,6 @@ wss.on('connection', (ws) => {
ws.on('close', () => clients.delete(ws));
ws.on('message', (data) => {
// User interaction event - write to stdout for Claude
const event = JSON.parse(data.toString());
console.log(JSON.stringify({ source: 'user-event', ...event }));
});
@@ -66,27 +80,30 @@ wss.on('connection', (ws) => {
// Serve newest screen with helper.js injected
app.get('/', (req, res) => {
const screenFile = getNewestScreen();
let html = screenFile ? fs.readFileSync(screenFile, 'utf-8') : WAITING_PAGE;
let html;
// Inject helper script before </body>
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
const injection = `<script>\n${helperScript}\n</script>`;
if (html.includes('</body>')) {
html = html.replace('</body>', `${injection}\n</body>`);
if (!screenFile) {
html = WAITING_PAGE;
} else {
html += injection;
const raw = fs.readFileSync(screenFile, 'utf-8');
html = isFullDocument(raw) ? raw : wrapInFrame(raw);
}
// Inject helper script
if (html.includes('</body>')) {
html = html.replace('</body>', `${helperInjection}\n</body>`);
} else {
html += helperInjection;
}
res.type('html').send(html);
});
// Watch for new or changed .html files in the directory
// Watch for new or changed .html files
chokidar.watch(SCREEN_DIR, { ignoreInitial: true })
.on('add', (filePath) => {
if (filePath.endsWith('.html')) {
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
// Notify all browsers to reload
clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'reload' }));