2026-03-06 12:55:35 -08:00
# Visual Brainstorming Companion Implementation Plan
2026-03-06 19:33:30 -08:00
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
2026-03-06 12:55:35 -08:00
**Goal:** Give Claude a browser-based visual companion for brainstorming sessions - show mockups, prototypes, and interactive choices alongside terminal conversation.
**Architecture:** Claude writes HTML to a temp file. A local Node.js server watches that file and serves it with an auto-injected helper library. User interactions flow via WebSocket to server stdout, which Claude sees in background task output.
**Tech Stack:** Node.js, Express, ws (WebSocket), chokidar (file watching)
---
## Task 1: Create the Server Foundation
**Files:**
- Create: `lib/brainstorm-server/index.js`
- Create: `lib/brainstorm-server/package.json`
**Step 1: Create package.json**
```json
{
"name" : "brainstorm-server" ,
"version" : "1.0.0" ,
"description" : "Visual brainstorming companion server for Claude Code" ,
"main" : "index.js" ,
"dependencies" : {
"chokidar" : "^3.5.3" ,
"express" : "^4.18.2" ,
"ws" : "^8.14.2"
}
}
```
**Step 2: Create minimal server that starts**
```javascript
const express = require ( 'express' );
const http = require ( 'http' );
const WebSocket = require ( 'ws' );
const chokidar = require ( 'chokidar' );
const fs = require ( 'fs' );
const path = require ( 'path' );
const PORT = process . env . BRAINSTORM_PORT || 3333 ;
const SCREEN_FILE = process . env . BRAINSTORM_SCREEN || '/tmp/brainstorm/screen.html' ;
const SCREEN_DIR = path . dirname ( SCREEN_FILE );
// 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>
<html>
<head>
<title>Brainstorm Companion</title>
<style>
body { font-family: system-ui, sans-serif; padding: 2rem; max-width: 800px; margin: 0 auto; }
h1 { color: #333; }
p { color: #666; }
</style>
</head>
<body>
<h1>Brainstorm Companion</h1>
<p>Waiting for Claude to push a screen...</p>
</body>
</html>` );
}
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 ) => {
clients . add ( 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 ({ type : 'user-event' , ... event }));
});
});
// Serve current screen with helper.js injected
app . get ( '/' , ( req , res ) => {
let html = fs . readFileSync ( SCREEN_FILE , 'utf-8' );
// 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>` );
} else {
html += injection ;
}
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' }));
}
});
});
server . listen ( PORT , '127.0.0.1' , () => {
console . log ( JSON . stringify ({ type : 'server-started' , port : PORT , url : `http://localhost: ${ PORT } ` }));
});
```
**Step 3: Run npm install**
Run: `cd lib/brainstorm-server && npm install`
Expected: Dependencies installed
**Step 4: Test server starts**
Run: `cd lib/brainstorm-server && timeout 3 node index.js || true`
Expected: See JSON with `server-started` and port info
**Step 5: Commit**
```bash
git add lib/brainstorm-server/
git commit -m "feat: add brainstorm server foundation"
```
---
## Task 2: Create the Helper Library
**Files:**
- Create: `lib/brainstorm-server/helper.js`
**Step 1: Create helper.js with event auto-capture**
```javascript
( function () {
const WS_URL = 'ws://' + window . location . host ;
let ws = null ;
let eventQueue = [];
function connect () {
ws = new WebSocket ( WS_URL );
ws . onopen = () => {
// Send any queued events
eventQueue . forEach ( e => ws . send ( JSON . stringify ( e )));
eventQueue = [];
};
ws . onmessage = ( msg ) => {
const data = JSON . parse ( msg . data );
if ( data . type === 'reload' ) {
window . location . reload ();
}
};
ws . onclose = () => {
// Reconnect after 1 second
setTimeout ( connect , 1000 );
};
}
function send ( event ) {
event . timestamp = Date . now ();
if ( ws && ws . readyState === WebSocket . OPEN ) {
ws . send ( JSON . stringify ( event ));
} else {
eventQueue . push ( event );
}
}
// Auto-capture clicks on interactive elements
document . addEventListener ( 'click' , ( e ) => {
const target = e . target . closest ( 'button, a, [data-choice], [role="button"], input[type="submit"]' );
if ( ! target ) return ;
// Don't capture regular link navigation
if ( target . tagName === 'A' && ! target . dataset . choice ) return ;
e . preventDefault ();
send ({
type : 'click' ,
text : target . textContent . trim (),
choice : target . dataset . choice || null ,
id : target . id || null ,
className : target . className || null
});
});
// Auto-capture form submissions
document . addEventListener ( 'submit' , ( e ) => {
e . preventDefault ();
const form = e . target ;
const formData = new FormData ( form );
const data = {};
formData . forEach (( value , key ) => { data [ key ] = value ; });
send ({
type : 'submit' ,
formId : form . id || null ,
formName : form . name || null ,
data : data
});
});
// Auto-capture input changes (debounced)
let inputTimeout = null ;
document . addEventListener ( 'input' , ( e ) => {
const target = e . target ;
if ( ! target . matches ( 'input, textarea, select' )) return ;
clearTimeout ( inputTimeout );
inputTimeout = setTimeout (() => {
send ({
type : 'input' ,
name : target . name || null ,
id : target . id || null ,
value : target . value ,
inputType : target . type || target . tagName . toLowerCase ()
});
}, 500 ); // 500ms debounce
});
// Expose for explicit use if needed
window . brainstorm = {
send : send ,
choice : ( value , metadata = {}) => send ({ type : 'choice' , value , ... metadata })
};
connect ();
})();
```
**Step 2: Verify helper.js is syntactically valid**
Run: `node -c lib/brainstorm-server/helper.js`
Expected: No syntax errors
**Step 3: Commit**
```bash
git add lib/brainstorm-server/helper.js
git commit -m "feat: add browser helper library for event capture"
```
---
## Task 3: Write Tests for the Server
**Files:**
- Create: `tests/brainstorm-server/server.test.js`
- Create: `tests/brainstorm-server/package.json`
**Step 1: Create test package.json**
```json
{
"name" : "brainstorm-server-tests" ,
"version" : "1.0.0" ,
"scripts" : {
"test" : "node server.test.js"
}
}
```
**Step 2: Write server tests**
```javascript
const { spawn } = require ( 'child_process' );
const http = require ( 'http' );
const WebSocket = require ( 'ws' );
const fs = require ( 'fs' );
const path = require ( 'path' );
const assert = require ( 'assert' );
const SERVER_PATH = path . join ( __dirname , '../../lib/brainstorm-server/index.js' );
const TEST_PORT = 3334 ;
const TEST_SCREEN = '/tmp/brainstorm-test/screen.html' ;
// Clean up test directory
function cleanup () {
if ( fs . existsSync ( path . dirname ( TEST_SCREEN ))) {
fs . rmSync ( path . dirname ( TEST_SCREEN ), { recursive : true });
}
}
async function sleep ( ms ) {
return new Promise ( resolve => setTimeout ( resolve , ms ));
}
async function fetch ( url ) {
return new Promise (( resolve , reject ) => {
http . get ( url , ( res ) => {
let data = '' ;
res . on ( 'data' , chunk => data += chunk );
res . on ( 'end' , () => resolve ({ status : res . statusCode , body : data }));
}). on ( 'error' , reject );
});
}
async function runTests () {
cleanup ();
// Start server
const server = spawn ( 'node' , [ SERVER_PATH ], {
env : { ... process . env , BRAINSTORM_PORT : TEST_PORT , BRAINSTORM_SCREEN : TEST_SCREEN }
});
let stdout = '' ;
server . stdout . on ( 'data' , ( data ) => { stdout += data . toString (); });
server . stderr . on ( 'data' , ( data ) => { console . error ( 'Server stderr:' , data . toString ()); });
await sleep ( 1000 ); // Wait for server to start
try {
// Test 1: Server starts and outputs JSON
console . log ( 'Test 1: Server startup message' );
assert ( stdout . includes ( 'server-started' ), 'Should output server-started' );
assert ( stdout . includes ( TEST_PORT . toString ()), 'Should include port' );
console . log ( ' PASS' );
// Test 2: GET / returns HTML with helper injected
console . log ( 'Test 2: Serves HTML with helper injected' );
const res = await fetch ( `http://localhost: ${ TEST_PORT } /` );
assert . strictEqual ( res . status , 200 );
assert ( res . body . includes ( 'brainstorm' ), 'Should include brainstorm content' );
assert ( res . body . includes ( 'WebSocket' ), 'Should have helper.js injected' );
console . log ( ' PASS' );
// Test 3: WebSocket connection and event relay
console . log ( 'Test 3: WebSocket relays events to stdout' );
stdout = '' ; // Reset stdout capture
const ws = new WebSocket ( `ws://localhost: ${ TEST_PORT } ` );
await new Promise ( resolve => ws . on ( 'open' , resolve ));
ws . send ( JSON . stringify ({ type : 'click' , text : 'Test Button' }));
await sleep ( 100 );
assert ( stdout . includes ( 'user-event' ), 'Should relay user events' );
assert ( stdout . includes ( 'Test Button' ), 'Should include event data' );
ws . close ();
console . log ( ' PASS' );
// Test 4: File change triggers reload notification
console . log ( 'Test 4: File change notifies browsers' );
const ws2 = new WebSocket ( `ws://localhost: ${ TEST_PORT } ` );
await new Promise ( resolve => ws2 . on ( 'open' , resolve ));
let gotReload = false ;
ws2 . on ( 'message' , ( data ) => {
const msg = JSON . parse ( data . toString ());
if ( msg . type === 'reload' ) gotReload = true ;
});
// Modify the screen file
fs . writeFileSync ( TEST_SCREEN , '<html><body>Updated</body></html>' );
await sleep ( 500 );
assert ( gotReload , 'Should send reload message on file change' );
ws2 . close ();
console . log ( ' PASS' );
console . log ( '\nAll tests passed!' );
} finally {
server . kill ();
cleanup ();
}
}
runTests (). catch ( err => {
console . error ( 'Test failed:' , err );
process . exit ( 1 );
});
```
**Step 3: Run tests**
Run: `cd tests/brainstorm-server && npm install ws && node server.test.js`
Expected: All tests pass
**Step 4: Commit**
```bash
git add tests/brainstorm-server/
git commit -m "test: add brainstorm server integration tests"
```
---
## Task 4: Add Visual Companion to Brainstorming Skill
**Files:**
- Modify: `skills/brainstorming/SKILL.md`
- Create: `skills/brainstorming/visual-companion.md` (supporting doc)
**Step 1: Create the supporting documentation**
Create `skills/brainstorming/visual-companion.md` :
```markdown
# Visual Companion Reference
## Starting the Server
Run as a background job:
```bash
node ${PLUGIN_ROOT}/lib/brainstorm-server/index.js
```
Tell the user: "I've started a visual companion at http://localhost:3333 - open it in a browser."
## Pushing Screens
Write HTML to `/tmp/brainstorm/screen.html` . The server watches this file and auto-refreshes the browser.
## Reading User Responses
Check the background task output for JSON events:
```json
{ "type" : "user-event" , "type" : "click" , "text" : "Option A" , "choice" : "optionA" , "timestamp" : 1234567890 }
{ "type" : "user-event" , "type" : "submit" , "data" :{ "notes" : "My feedback" }, "timestamp" : 1234567891 }
```
Event types:
- **click**: User clicked button or `data-choice` element
- **submit**: User submitted form (includes all form data)
- **input**: User typed in field (debounced 500ms)
## HTML Patterns
### Choice Cards
```html
< div class = "options" >
< button data-choice = "optionA" >
< h3 > Option A</ h3 >
< p > Description</ p >
</ button >
< button data-choice = "optionB" >
< h3 > Option B</ h3 >
< p > Description</ p >
</ button >
</ div >
```
### Interactive Mockup
```html
< div class = "mockup" >
< header data-choice = "header" > App Header</ header >
< nav data-choice = "nav" > Navigation</ nav >
< main data-choice = "main" > Content</ main >
</ div >
```
### Form with Notes
```html
< form >
< label > Priority: < input type = "range" name = "priority" min = "1" max = "5" ></ label >
< textarea name = "notes" placeholder = "Additional thoughts..." ></ textarea >
< button type = "submit" > Submit</ button >
</ form >
```
### Explicit JavaScript
```html
< button onclick = "brainstorm.choice('custom', {extra: 'data'})" > Custom</ button >
```
```
**Step 2: Add visual companion section to brainstorming skill**
Add after "Key Principles" in `skills/brainstorming/SKILL.md`:
```markdown
## Visual Companion (Optional)
When brainstorming involves visual elements - UI mockups, wireframes, interactive prototypes - use the browser-based visual companion.
**When to use:**
- Presenting UI/UX options that benefit from visual comparison
- Showing wireframes or layout options
- Gathering structured feedback (ratings, forms)
- Prototyping click interactions
**How it works:**
1. Start the server as a background job
2. Tell user to open http://localhost:3333
3. Write HTML to `/tmp/brainstorm/screen.html` (auto-refreshes)
4. Check background task output for user interactions
The terminal remains the primary conversation interface. The browser is a visual aid.
**Reference:** See `visual-companion.md` in this skill directory for HTML patterns and API details.
```
**Step 3: Verify the edits**
Run: `grep -A5 "Visual Companion" skills/brainstorming/SKILL.md`
Expected: Shows the new section
**Step 4: Commit**
```bash
git add skills/brainstorming/
git commit -m "feat: add visual companion to brainstorming skill"
```
---
## Task 5: Add Server to Plugin Ignore (Optional Cleanup)
**Files:**
- Check if `.gitignore` needs node_modules exclusion for lib/brainstorm-server
**Step 1: Check current gitignore**
Run: `cat .gitignore 2>/dev/null || echo "No .gitignore"`
**Step 2: Add node_modules if needed**
If not already present, add:
```
lib/brainstorm-server/node_modules/
```
**Step 3: Commit if changed**
```bash
git add .gitignore
git commit -m "chore: ignore brainstorm-server node_modules"
```
---
## Summary
After completing all tasks:
1. **Server** at `lib/brainstorm-server/` - Node.js server that watches HTML file and relays events
2. **Helper library** auto-injected - captures clicks, forms, inputs
3. **Tests** at `tests/brainstorm-server/` - verifies server behavior
4. **Brainstorming skill** updated with visual companion section and `visual-companion.md` reference doc
**To use:**
1. Start server as background job: `node lib/brainstorm-server/index.js &`
2. Tell user to open `http://localhost:3333`
3. Write HTML to `/tmp/brainstorm/screen.html`
4. Check task output for user events