Use semantic filenames for visual companion screens

Server now watches directory for new .html files instead of a single
screen file. Claude writes to semantically named files like
platform.html, style.html, layout.html - each screen is a new file.

Benefits:
- No need to read before write (files are always new)
- Semantic filenames describe what's on screen
- History preserved in directory for debugging
- Server serves newest file by mtime automatically

Updated: index.js, start-server.sh, and all documentation.
This commit is contained in:
Jesse Vincent
2026-01-17 19:32:02 -08:00
parent e5a7c05528
commit b3e922c10a
5 changed files with 80 additions and 43 deletions

View File

@@ -15,27 +15,33 @@ Use the visual companion when you need to show:
## Lifecycle
```bash
# Start server (returns JSON with URL and session paths)
# Start server (returns JSON with URL and session directory)
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh
# Returns: {"type":"server-started","port":52341,"url":"http://localhost:52341",
# "screen_dir":"/tmp/brainstorm-12345-1234567890",
# "screen_file":"/tmp/brainstorm-12345-1234567890/screen.html"}
# "screen_dir":"/tmp/brainstorm-12345-1234567890"}
# Save screen_dir and screen_file from response!
# Save screen_dir from response!
# Tell user to open the URL in their browser
# Write screens to screen_file (auto-refreshes)
# Wait for user feedback:
# 1. Start watcher in background
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-event.sh $SCREEN_DIR/.server.log
# 2. Immediately call TaskOutput(task_id, block=true) to wait for completion
# For each screen:
# 1. Start watcher in background FIRST (avoids race condition)
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-feedback.sh $SCREEN_DIR
# 2. Write HTML to a NEW file in screen_dir (e.g., platform.html, style.html)
# Server automatically serves the newest file by modification time
# 3. Call TaskOutput(task_id, block=true, timeout=600000) to wait for feedback
# When done, stop server (pass screen_dir)
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR
```
## File Naming
- **Use semantic names**: `platform.html`, `visual-style.html`, `layout.html`, `controls.html`
- **Never reuse filenames** - each screen must be a new file
- **For iterations**: append version suffix like `layout-v2.html`, `layout-v3.html`
- Server automatically serves the newest `.html` file by modification time
## Writing Screens
Copy the frame template structure but replace `#claude-content` with your content:

View File

@@ -7,17 +7,29 @@ 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_FILE = process.env.BRAINSTORM_SCREEN || '/tmp/brainstorm/screen.html';
const SCREEN_DIR = path.dirname(SCREEN_FILE);
const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
// Ensure screen directory exists
if (!fs.existsSync(SCREEN_DIR)) {
fs.mkdirSync(SCREEN_DIR, { recursive: true });
}
// Create default screen if none exists
if (!fs.existsSync(SCREEN_FILE)) {
fs.writeFileSync(SCREEN_FILE, `<!DOCTYPE html>
// Find the newest .html file in the directory by mtime
function getNewestScreen() {
const files = fs.readdirSync(SCREEN_DIR)
.filter(f => f.endsWith('.html'))
.map(f => ({
name: f,
path: path.join(SCREEN_DIR, f),
mtime: fs.statSync(path.join(SCREEN_DIR, f)).mtime.getTime()
}))
.sort((a, b) => b.mtime - a.mtime);
return files.length > 0 ? files[0].path : null;
}
// Default waiting page (served when no screens exist yet)
const WAITING_PAGE = `<!DOCTYPE html>
<html>
<head>
<title>Brainstorm Companion</title>
@@ -31,8 +43,7 @@ if (!fs.existsSync(SCREEN_FILE)) {
<h1>Brainstorm Companion</h1>
<p>Waiting for Claude to push a screen...</p>
</body>
</html>`);
}
</html>`;
const app = express();
const server = http.createServer(app);
@@ -52,9 +63,10 @@ wss.on('connection', (ws) => {
});
});
// Serve current screen with helper.js injected
// Serve newest screen with helper.js injected
app.get('/', (req, res) => {
let html = fs.readFileSync(SCREEN_FILE, 'utf-8');
const screenFile = getNewestScreen();
let html = screenFile ? fs.readFileSync(screenFile, 'utf-8') : WAITING_PAGE;
// Inject helper script before </body>
const helperScript = fs.readFileSync(path.join(__dirname, 'helper.js'), 'utf-8');
@@ -69,23 +81,35 @@ app.get('/', (req, res) => {
res.type('html').send(html);
});
// Watch for screen file changes
chokidar.watch(SCREEN_FILE).on('change', () => {
console.log(JSON.stringify({ type: 'screen-updated', file: SCREEN_FILE }));
// Notify all browsers to reload
clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'reload' }));
// Watch for new or changed .html files in the directory
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' }));
}
});
}
})
.on('change', (filePath) => {
if (filePath.endsWith('.html')) {
console.log(JSON.stringify({ type: 'screen-updated', file: filePath }));
clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'reload' }));
}
});
}
});
});
server.listen(PORT, '127.0.0.1', () => {
console.log(JSON.stringify({
type: 'server-started',
port: PORT,
url: `http://localhost:${PORT}`,
screen_dir: SCREEN_DIR,
screen_file: SCREEN_FILE
screen_dir: SCREEN_DIR
}));
});

View File

@@ -11,7 +11,6 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# Generate unique session directory
SESSION_ID="$$-$(date +%s)"
SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}"
SCREEN_FILE="${SCREEN_DIR}/screen.html"
PID_FILE="${SCREEN_DIR}/.server.pid"
LOG_FILE="${SCREEN_DIR}/.server.log"
@@ -27,7 +26,7 @@ fi
# Start server, capturing output to log file
cd "$SCRIPT_DIR"
BRAINSTORM_SCREEN="$SCREEN_FILE" node index.js > "$LOG_FILE" 2>&1 &
BRAINSTORM_DIR="$SCREEN_DIR" node index.js > "$LOG_FILE" 2>&1 &
SERVER_PID=$!
echo "$SERVER_PID" > "$PID_FILE"