mirror of
https://github.com/obra/superpowers.git
synced 2026-04-19 21:42:41 +00:00
Compare commits
19 Commits
v4.1.0
...
fix/window
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3d478dc47 | ||
|
|
e147c303c0 | ||
|
|
7a6b4c14d5 | ||
|
|
2b8814f7d9 | ||
|
|
b16369cae2 | ||
|
|
e9263c9754 | ||
|
|
de2e15242c | ||
|
|
70c2d06c4e | ||
|
|
b98afbd74f | ||
|
|
2a61167b02 | ||
|
|
94d5f4a817 | ||
|
|
209fcec3b9 | ||
|
|
fccb5b4b8f | ||
|
|
15d0f2a8f6 | ||
|
|
cc585ad4d5 | ||
|
|
7e86703081 | ||
|
|
c536926f03 | ||
|
|
333eaa3281 | ||
|
|
74a55c0bf4 |
14
.codex/superpowers-codex.cmd
Normal file
14
.codex/superpowers-codex.cmd
Normal file
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
REM Windows shim for the extensionless Node.js launcher (superpowers-codex).
|
||||
REM
|
||||
REM Windows cannot execute extensionless scripts with shebangs, so this wrapper
|
||||
REM invokes Node.js directly.
|
||||
REM
|
||||
REM Usage:
|
||||
REM superpowers-codex.cmd bootstrap
|
||||
REM superpowers-codex.cmd use-skill superpowers:brainstorming
|
||||
REM superpowers-codex.cmd find-skills
|
||||
|
||||
node "%~dp0superpowers-codex" %*
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.worktrees/
|
||||
.private-journal/
|
||||
.claude/
|
||||
node_modules/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Superpowers Release Notes
|
||||
|
||||
## v4.1.0 (2026-01-23)
|
||||
## Unreleased
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
@@ -22,12 +22,46 @@ The previous bootstrap injection method using `session.prompt({ noReply: true })
|
||||
- Added comprehensive Windows installation docs for cmd.exe, PowerShell, and Git Bash
|
||||
- Documented proper symlink vs junction usage for each platform
|
||||
|
||||
**Claude Code: Fixed Windows hook execution for Claude Code 2.1.x**
|
||||
### New Features
|
||||
|
||||
**Visual companion for brainstorming skill**
|
||||
|
||||
Added optional browser-based visual companion for brainstorming sessions. When users have a browser available, brainstorming can display interactive screens showing current phase, questions, and design decisions in a more readable format than terminal output.
|
||||
|
||||
Components:
|
||||
- `lib/brainstorm-server/` - WebSocket server for real-time updates
|
||||
- `skills/brainstorming/visual-companion.md` - Integration guide
|
||||
- Helper scripts for session management with proper isolation
|
||||
- Browser helper library for event capture
|
||||
|
||||
The visual companion is opt-in and falls back gracefully to terminal-only operation.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
**Fixed Windows hook execution for Claude Code 2.1.x**
|
||||
|
||||
Claude Code 2.1.x changed how hooks execute on Windows: it now auto-detects `.sh` files in commands and prepends `bash `. This broke the polyglot wrapper pattern because `bash "run-hook.cmd" session-start.sh` tries to execute the .cmd file as a bash script.
|
||||
|
||||
Fix: hooks.json now calls session-start.sh directly. Claude Code 2.1.x handles the bash invocation automatically. Also added .gitattributes to enforce LF line endings for shell scripts (fixes CRLF issues on Windows checkout).
|
||||
|
||||
**Fixed Windows Codex launcher (#243, #285)**
|
||||
|
||||
Windows cannot execute extensionless scripts with shebangs, so the `superpowers-codex` script would either open an "Open with" dialog or produce no output in PowerShell.
|
||||
|
||||
Fix: Added `.codex/superpowers-codex.cmd` wrapper that invokes Node.js directly. Updated docs with Windows-specific installation and usage instructions.
|
||||
|
||||
### Improvements
|
||||
|
||||
**Instruction priority clarified in using-superpowers**
|
||||
|
||||
Added explicit instruction priority hierarchy to prevent conflicts with user preferences:
|
||||
|
||||
1. User's explicit instructions (CLAUDE.md, direct requests) — highest priority
|
||||
2. Superpowers skills — override default system behavior where they conflict
|
||||
3. Default system prompt — lowest priority
|
||||
|
||||
This ensures users remain in control. If CLAUDE.md says "don't use TDD" and a skill says "always use TDD," CLAUDE.md wins.
|
||||
|
||||
---
|
||||
|
||||
## v4.0.3 (2025-12-26)
|
||||
|
||||
@@ -19,13 +19,27 @@ Fetch and follow instructions from https://raw.githubusercontent.com/obra/superp
|
||||
|
||||
### Installation Steps
|
||||
|
||||
#### 1. Clone Superpowers
|
||||
#### macOS / Linux
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.codex/superpowers
|
||||
git clone https://github.com/obra/superpowers.git ~/.codex/superpowers
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
**Command Prompt:**
|
||||
```cmd
|
||||
mkdir "%USERPROFILE%\.codex\superpowers"
|
||||
git clone https://github.com/obra/superpowers.git "%USERPROFILE%\.codex\superpowers"
|
||||
```
|
||||
|
||||
**PowerShell:**
|
||||
```powershell
|
||||
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\.codex\superpowers"
|
||||
git clone https://github.com/obra/superpowers.git "$env:USERPROFILE\.codex\superpowers"
|
||||
```
|
||||
|
||||
#### 2. Install Bootstrap
|
||||
|
||||
The bootstrap file is included in the repository at `.codex/superpowers-bootstrap.md`. Codex will automatically use it from the cloned location.
|
||||
@@ -34,12 +48,20 @@ The bootstrap file is included in the repository at `.codex/superpowers-bootstra
|
||||
|
||||
Tell Codex:
|
||||
|
||||
**macOS / Linux:**
|
||||
```
|
||||
Run ~/.codex/superpowers/.codex/superpowers-codex find-skills to show available skills
|
||||
```
|
||||
|
||||
**Windows:**
|
||||
```
|
||||
Run ~/.codex/superpowers/.codex/superpowers-codex.cmd find-skills to show available skills
|
||||
```
|
||||
|
||||
You should see a list of available skills with descriptions.
|
||||
|
||||
> **Note:** On Windows, always use the `.cmd` extension when running superpowers-codex commands.
|
||||
|
||||
## Usage
|
||||
|
||||
### Finding Skills
|
||||
@@ -126,12 +148,26 @@ git pull
|
||||
2. Check CLI works: `~/.codex/superpowers/.codex/superpowers-codex find-skills`
|
||||
3. Verify skills have SKILL.md files
|
||||
|
||||
### CLI script not executable
|
||||
### CLI script not executable (macOS/Linux)
|
||||
|
||||
```bash
|
||||
chmod +x ~/.codex/superpowers/.codex/superpowers-codex
|
||||
```
|
||||
|
||||
### Windows: "Open with" dialog or no output
|
||||
|
||||
On Windows, you must use the `.cmd` wrapper:
|
||||
|
||||
```cmd
|
||||
~/.codex/superpowers/.codex/superpowers-codex.cmd find-skills
|
||||
```
|
||||
|
||||
Or invoke with Node directly:
|
||||
|
||||
```cmd
|
||||
node "%USERPROFILE%\.codex\superpowers\.codex\superpowers-codex" find-skills
|
||||
```
|
||||
|
||||
### Node.js errors
|
||||
|
||||
The CLI script requires Node.js. Verify:
|
||||
@@ -140,7 +176,7 @@ The CLI script requires Node.js. Verify:
|
||||
node --version
|
||||
```
|
||||
|
||||
Should show v14 or higher (v18+ recommended for ES module support).
|
||||
Should show v14 or higher (v18+ recommended).
|
||||
|
||||
## Getting Help
|
||||
|
||||
|
||||
571
docs/plans/2026-01-17-visual-brainstorming.md
Normal file
571
docs/plans/2026-01-17-visual-brainstorming.md
Normal file
@@ -0,0 +1,571 @@
|
||||
# Visual Brainstorming Companion Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**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
|
||||
212
lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md
Normal file
212
lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# Visual Companion Instructions for Claude
|
||||
|
||||
This document explains how to use the brainstorm visual companion to show mockups, designs, and options to users without resorting to ASCII art.
|
||||
|
||||
## When to Use
|
||||
|
||||
Use the visual companion when you need to show:
|
||||
- **UI mockups** - layouts, navigation patterns, component designs
|
||||
- **Design comparisons** - "Which of these 3 approaches works better?"
|
||||
- **Interactive prototypes** - clickable wireframes
|
||||
- **Visual choices** - anything where seeing beats describing
|
||||
|
||||
**Don't use it for:** simple text questions, code review, or when the user prefers terminal-only interaction.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
```bash
|
||||
# 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"}
|
||||
|
||||
# Save screen_dir from response!
|
||||
|
||||
# Tell user to open the URL in their browser
|
||||
|
||||
# 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:
|
||||
|
||||
```html
|
||||
<div id="claude-content">
|
||||
<h2>Your Question</h2>
|
||||
<p class="subtitle">Brief context</p>
|
||||
|
||||
<!-- Your content here -->
|
||||
</div>
|
||||
```
|
||||
|
||||
The frame template (`frame-template.html`) includes CSS for:
|
||||
- OS-aware light/dark theming
|
||||
- Fixed header and feedback footer
|
||||
- Common UI patterns (see below)
|
||||
|
||||
## CSS Helper Classes
|
||||
|
||||
### Options (A/B/C choices)
|
||||
|
||||
```html
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Option Title</h3>
|
||||
<p>Description of this option</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- More options... -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### Cards (visual designs)
|
||||
|
||||
```html
|
||||
<div class="cards">
|
||||
<div class="card" data-choice="design1" onclick="toggleSelect(this)">
|
||||
<div class="card-image">
|
||||
<!-- Put mockup content here -->
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>Design Name</h3>
|
||||
<p>Brief description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Mockup Container
|
||||
|
||||
```html
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Preview: Dashboard Layout</div>
|
||||
<div class="mockup-body">
|
||||
<!-- Your mockup HTML -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Split View (side-by-side)
|
||||
|
||||
```html
|
||||
<div class="split">
|
||||
<div class="mockup"><!-- Left side --></div>
|
||||
<div class="mockup"><!-- Right side --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pros/Cons
|
||||
|
||||
```html
|
||||
<div class="pros-cons">
|
||||
<div class="pros">
|
||||
<h4>Pros</h4>
|
||||
<ul>
|
||||
<li>Benefit one</li>
|
||||
<li>Benefit two</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="cons">
|
||||
<h4>Cons</h4>
|
||||
<ul>
|
||||
<li>Drawback one</li>
|
||||
<li>Drawback two</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Inline Mockup Elements
|
||||
|
||||
```html
|
||||
<div class="mock-nav">Logo | Home | About | Contact</div>
|
||||
<div style="display: flex;">
|
||||
<div class="mock-sidebar">Navigation</div>
|
||||
<div class="mock-content">Main content area</div>
|
||||
</div>
|
||||
<button class="mock-button">Action Button</button>
|
||||
<input class="mock-input" placeholder="Input field">
|
||||
```
|
||||
|
||||
## User Feedback
|
||||
|
||||
When the user clicks Send, you receive JSON like:
|
||||
|
||||
```json
|
||||
{"choice": "a", "feedback": "I like this but make the header smaller"}
|
||||
```
|
||||
|
||||
- `choice` - which option/card they selected (from `data-choice` attribute)
|
||||
- `feedback` - any notes they typed
|
||||
|
||||
## Example: Design Comparison
|
||||
|
||||
```html
|
||||
<div id="claude-content">
|
||||
<h2>Which blog layout works better?</h2>
|
||||
<p class="subtitle">Consider readability and visual hierarchy</p>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card" data-choice="classic" onclick="toggleSelect(this)">
|
||||
<div class="card-image">
|
||||
<div style="padding: 1rem;">
|
||||
<div class="mock-nav">Blog Title</div>
|
||||
<div style="padding: 1rem;">
|
||||
<h3 style="margin-bottom: 0.5rem;">Post Title</h3>
|
||||
<p style="color: var(--text-secondary); font-size: 0.9rem;">
|
||||
Content preview text goes here...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>Classic Layout</h3>
|
||||
<p>Traditional blog with posts in a single column</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" data-choice="magazine" onclick="toggleSelect(this)">
|
||||
<div class="card-image">
|
||||
<div style="padding: 1rem;">
|
||||
<div class="mock-nav">Blog Title</div>
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; padding: 0.5rem;">
|
||||
<div class="placeholder" style="padding: 1rem;">Featured</div>
|
||||
<div class="placeholder" style="padding: 0.5rem;">Post</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h3>Magazine Layout</h3>
|
||||
<p>Grid-based with featured posts</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
1. **Keep mockups simple** - Focus on layout and structure, not pixel-perfect design
|
||||
2. **Use placeholders** - The `.placeholder` class works great for content areas
|
||||
3. **Label clearly** - Use `.mockup-header` to explain what each mockup shows
|
||||
4. **Limit choices** - 2-4 options is ideal; more gets overwhelming
|
||||
5. **Provide context** - Use `.subtitle` to explain what you're asking
|
||||
6. **Regenerate fully** - Write the complete HTML each turn; don't try to patch
|
||||
259
lib/brainstorm-server/frame-template.html
Normal file
259
lib/brainstorm-server/frame-template.html
Normal file
@@ -0,0 +1,259 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Brainstorm Companion</title>
|
||||
<style>
|
||||
/*
|
||||
* BRAINSTORM COMPANION FRAME TEMPLATE
|
||||
*
|
||||
* This template provides a consistent frame with:
|
||||
* - OS-aware light/dark theming
|
||||
* - Fixed header and feedback footer
|
||||
* - Scrollable main content area
|
||||
* - CSS helpers for common UI patterns
|
||||
*
|
||||
* CLAUDE: Replace the contents of #claude-content with your content.
|
||||
* Keep the header, main wrapper, and feedback-footer intact.
|
||||
*/
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html, body { height: 100%; overflow: hidden; }
|
||||
|
||||
/* ===== THEME VARIABLES ===== */
|
||||
:root {
|
||||
--bg-primary: #f5f5f7;
|
||||
--bg-secondary: #ffffff;
|
||||
--bg-tertiary: #e5e5e7;
|
||||
--border: #d1d1d6;
|
||||
--text-primary: #1d1d1f;
|
||||
--text-secondary: #86868b;
|
||||
--text-tertiary: #aeaeb2;
|
||||
--accent: #0071e3;
|
||||
--accent-hover: #0077ed;
|
||||
--success: #34c759;
|
||||
--warning: #ff9f0a;
|
||||
--error: #ff3b30;
|
||||
--selected-bg: #e8f4fd;
|
||||
--selected-border: #0071e3;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-primary: #1d1d1f;
|
||||
--bg-secondary: #2d2d2f;
|
||||
--bg-tertiary: #3d3d3f;
|
||||
--border: #424245;
|
||||
--text-primary: #f5f5f7;
|
||||
--text-secondary: #86868b;
|
||||
--text-tertiary: #636366;
|
||||
--accent: #0a84ff;
|
||||
--accent-hover: #409cff;
|
||||
--selected-bg: rgba(10, 132, 255, 0.15);
|
||||
--selected-border: #0a84ff;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ===== FRAME STRUCTURE ===== */
|
||||
.header {
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.5rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.header h1 { font-size: 0.85rem; font-weight: 500; color: var(--text-secondary); }
|
||||
.header .status { font-size: 0.7rem; color: var(--success); display: flex; align-items: center; gap: 0.4rem; }
|
||||
.header .status::before { content: ''; width: 6px; height: 6px; background: var(--success); border-radius: 50%; }
|
||||
|
||||
.main { flex: 1; overflow-y: auto; }
|
||||
#claude-content { padding: 2rem; min-height: 100%; }
|
||||
|
||||
.feedback-footer {
|
||||
background: var(--bg-secondary);
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 0.75rem 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.feedback-footer label { display: block; font-size: 0.65rem; color: var(--text-secondary); margin-bottom: 0.4rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
.feedback-row { display: flex; gap: 0.5rem; }
|
||||
.feedback-footer textarea {
|
||||
flex: 1;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
resize: none;
|
||||
height: 36px;
|
||||
}
|
||||
.feedback-footer textarea:focus { outline: none; border-color: var(--accent); }
|
||||
.feedback-footer button {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.feedback-footer button:hover { background: var(--accent-hover); }
|
||||
|
||||
/* ===== TYPOGRAPHY ===== */
|
||||
h2 { font-size: 1.5rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
h3 { font-size: 1.1rem; font-weight: 600; margin-bottom: 0.25rem; }
|
||||
.subtitle { color: var(--text-secondary); margin-bottom: 1.5rem; }
|
||||
.section { margin-bottom: 2rem; }
|
||||
.label { font-size: 0.7rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 0.5rem; }
|
||||
|
||||
/* ===== OPTIONS (for A/B/C choices) ===== */
|
||||
.options { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.option {
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 1rem 1.25rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
.option:hover { border-color: var(--accent); }
|
||||
.option.selected { background: var(--selected-bg); border-color: var(--selected-border); }
|
||||
.option .letter {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-secondary);
|
||||
width: 1.75rem; height: 1.75rem;
|
||||
border-radius: 6px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-weight: 600; font-size: 0.85rem; flex-shrink: 0;
|
||||
}
|
||||
.option.selected .letter { background: var(--accent); color: white; }
|
||||
.option .content { flex: 1; }
|
||||
.option .content h3 { font-size: 0.95rem; margin-bottom: 0.15rem; }
|
||||
.option .content p { color: var(--text-secondary); font-size: 0.85rem; margin: 0; }
|
||||
|
||||
/* ===== CARDS (for showing designs/mockups) ===== */
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; }
|
||||
.card {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.card:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
|
||||
.card.selected { border-color: var(--selected-border); border-width: 2px; }
|
||||
.card-image { background: var(--bg-tertiary); aspect-ratio: 16/10; display: flex; align-items: center; justify-content: center; }
|
||||
.card-body { padding: 1rem; }
|
||||
.card-body h3 { margin-bottom: 0.25rem; }
|
||||
.card-body p { color: var(--text-secondary); font-size: 0.85rem; }
|
||||
|
||||
/* ===== MOCKUP CONTAINER ===== */
|
||||
.mockup {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.mockup-header {
|
||||
background: var(--bg-tertiary);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.mockup-body { padding: 1.5rem; }
|
||||
|
||||
/* ===== SPLIT VIEW (side-by-side comparison) ===== */
|
||||
.split { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; }
|
||||
@media (max-width: 700px) { .split { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ===== PROS/CONS ===== */
|
||||
.pros-cons { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin: 1rem 0; }
|
||||
.pros, .cons { background: var(--bg-secondary); border-radius: 8px; padding: 1rem; }
|
||||
.pros h4 { color: var(--success); font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.cons h4 { color: var(--error); font-size: 0.85rem; margin-bottom: 0.5rem; }
|
||||
.pros ul, .cons ul { margin-left: 1.25rem; font-size: 0.85rem; color: var(--text-secondary); }
|
||||
.pros li, .cons li { margin-bottom: 0.25rem; }
|
||||
|
||||
/* ===== PLACEHOLDER (for mockup areas) ===== */
|
||||
.placeholder {
|
||||
background: var(--bg-tertiary);
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* ===== INLINE MOCKUP ELEMENTS ===== */
|
||||
.mock-nav { background: var(--accent); color: white; padding: 0.75rem 1rem; display: flex; gap: 1.5rem; font-size: 0.9rem; }
|
||||
.mock-sidebar { background: var(--bg-tertiary); padding: 1rem; min-width: 180px; }
|
||||
.mock-content { padding: 1.5rem; flex: 1; }
|
||||
.mock-button { background: var(--accent); color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; }
|
||||
.mock-input { background: var(--bg-primary); border: 1px solid var(--border); border-radius: 6px; padding: 0.5rem; width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>Brainstorm Companion</h1>
|
||||
<div class="status">Connected</div>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div id="claude-content">
|
||||
<!-- CLAUDE: Replace this content -->
|
||||
<h2>Visual Brainstorming</h2>
|
||||
<p class="subtitle">Claude will show mockups and options here.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feedback-footer">
|
||||
<label>Feedback for Claude</label>
|
||||
<div class="feedback-row">
|
||||
<textarea id="feedback" placeholder="Add notes (optional)..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send()}"></textarea>
|
||||
<button onclick="send()">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedChoice = null;
|
||||
|
||||
function toggleSelect(el) {
|
||||
const container = el.closest('.options') || el.closest('.cards');
|
||||
if (container) {
|
||||
container.querySelectorAll('.option, .card').forEach(o => o.classList.remove('selected'));
|
||||
}
|
||||
el.classList.add('selected');
|
||||
selectedChoice = el.dataset.choice;
|
||||
}
|
||||
|
||||
function send() {
|
||||
const feedbackEl = document.getElementById('feedback');
|
||||
const feedback = feedbackEl.value.trim();
|
||||
const payload = {};
|
||||
if (selectedChoice) payload.choice = selectedChoice;
|
||||
if (feedback) payload.feedback = feedback;
|
||||
if (Object.keys(payload).length === 0) return;
|
||||
brainstorm.sendToClaude(payload);
|
||||
feedbackEl.value = '';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
115
lib/brainstorm-server/helper.js
Normal file
115
lib/brainstorm-server/helper.js
Normal file
@@ -0,0 +1,115 @@
|
||||
(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
|
||||
});
|
||||
|
||||
// Send to Claude - triggers server to exit and return all events
|
||||
function sendToClaude(feedback) {
|
||||
send({
|
||||
type: 'send-to-claude',
|
||||
feedback: feedback || null
|
||||
});
|
||||
// Show confirmation to user
|
||||
document.body.innerHTML = `
|
||||
<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: system-ui, sans-serif;">
|
||||
<div style="text-align: center; color: #666;">
|
||||
<h2 style="color: #333;">✓ Sent to Claude</h2>
|
||||
<p>Return to the terminal to see Claude's response.</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Expose for explicit use if needed
|
||||
window.brainstorm = {
|
||||
send: send,
|
||||
choice: (value, metadata = {}) => send({ type: 'choice', value, ...metadata }),
|
||||
sendToClaude: sendToClaude
|
||||
};
|
||||
|
||||
connect();
|
||||
})();
|
||||
115
lib/brainstorm-server/index.js
Normal file
115
lib/brainstorm-server/index.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const WebSocket = require('ws');
|
||||
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 });
|
||||
}
|
||||
|
||||
// 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>
|
||||
<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({ source: 'user-event', ...event }));
|
||||
});
|
||||
});
|
||||
|
||||
// Serve newest screen with helper.js injected
|
||||
app.get('/', (req, res) => {
|
||||
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');
|
||||
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 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
|
||||
}));
|
||||
});
|
||||
1036
lib/brainstorm-server/package-lock.json
generated
Normal file
1036
lib/brainstorm-server/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
lib/brainstorm-server/package.json
Normal file
11
lib/brainstorm-server/package.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
45
lib/brainstorm-server/start-server.sh
Executable file
45
lib/brainstorm-server/start-server.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
# Start the brainstorm server and output connection info
|
||||
# Usage: start-server.sh
|
||||
#
|
||||
# Starts server on a random high port, outputs JSON with URL
|
||||
# Each session gets its own temp directory to avoid conflicts
|
||||
# Server runs in background, PID saved for cleanup
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
# Generate unique session directory
|
||||
SESSION_ID="$$-$(date +%s)"
|
||||
SCREEN_DIR="/tmp/brainstorm-${SESSION_ID}"
|
||||
PID_FILE="${SCREEN_DIR}/.server.pid"
|
||||
LOG_FILE="${SCREEN_DIR}/.server.log"
|
||||
|
||||
# Create fresh session directory
|
||||
mkdir -p "$SCREEN_DIR"
|
||||
|
||||
# Kill any existing server
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
old_pid=$(cat "$PID_FILE")
|
||||
kill "$old_pid" 2>/dev/null
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
|
||||
# Start server, capturing output to log file
|
||||
cd "$SCRIPT_DIR"
|
||||
BRAINSTORM_DIR="$SCREEN_DIR" node index.js > "$LOG_FILE" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
echo "$SERVER_PID" > "$PID_FILE"
|
||||
|
||||
# Wait for server-started message (check log file)
|
||||
for i in {1..50}; do
|
||||
if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
|
||||
# Extract and output the server-started line
|
||||
grep "server-started" "$LOG_FILE" | head -1
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
# Timeout - server didn't start
|
||||
echo '{"error": "Server failed to start within 5 seconds"}'
|
||||
exit 1
|
||||
22
lib/brainstorm-server/stop-server.sh
Executable file
22
lib/brainstorm-server/stop-server.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
# Stop the brainstorm server and clean up session directory
|
||||
# Usage: stop-server.sh <screen_dir>
|
||||
|
||||
SCREEN_DIR="$1"
|
||||
|
||||
if [[ -z "$SCREEN_DIR" ]]; then
|
||||
echo '{"error": "Usage: stop-server.sh <screen_dir>"}'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PID_FILE="${SCREEN_DIR}/.server.pid"
|
||||
|
||||
if [[ -f "$PID_FILE" ]]; then
|
||||
pid=$(cat "$PID_FILE")
|
||||
kill "$pid" 2>/dev/null
|
||||
# Clean up session directory
|
||||
rm -rf "$SCREEN_DIR"
|
||||
echo '{"status": "stopped"}'
|
||||
else
|
||||
echo '{"status": "not_running"}'
|
||||
fi
|
||||
27
lib/brainstorm-server/wait-for-feedback.sh
Executable file
27
lib/brainstorm-server/wait-for-feedback.sh
Executable file
@@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Wait for user feedback from the brainstorm browser
|
||||
# Usage: wait-for-feedback.sh <screen_dir>
|
||||
#
|
||||
# Blocks until user sends feedback, then outputs the JSON.
|
||||
# Write HTML to screen_file BEFORE calling this.
|
||||
|
||||
SCREEN_DIR="${1:?Usage: wait-for-feedback.sh <screen_dir>}"
|
||||
LOG_FILE="${SCREEN_DIR}/.server.log"
|
||||
|
||||
if [[ ! -d "$SCREEN_DIR" ]]; then
|
||||
echo '{"error": "Screen directory not found"}' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Record current position in log file
|
||||
LOG_POS=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)
|
||||
|
||||
# Poll for new lines containing the event
|
||||
while true; do
|
||||
RESULT=$(tail -n +$((LOG_POS + 1)) "$LOG_FILE" 2>/dev/null | grep -m 1 "send-to-claude")
|
||||
if [[ -n "$RESULT" ]]; then
|
||||
echo "$RESULT"
|
||||
exit 0
|
||||
fi
|
||||
sleep 0.2
|
||||
done
|
||||
@@ -52,3 +52,86 @@ Start by understanding the current project context, then ask questions one at a
|
||||
- **Explore alternatives** - Always propose 2-3 approaches before settling
|
||||
- **Incremental validation** - Present design in sections, validate each
|
||||
- **Be flexible** - Go back and clarify when something doesn't make sense
|
||||
|
||||
## Visual Companion (Claude Code Only)
|
||||
|
||||
A browser-based visual companion for showing mockups, diagrams, and options. Use it whenever visual representation makes feedback easier. **Only works in Claude Code.**
|
||||
|
||||
### When to Use
|
||||
|
||||
Use the visual companion when seeing beats describing:
|
||||
- **UI mockups** - layouts, navigation, component designs
|
||||
- **Architecture diagrams** - system components, data flow, relationships
|
||||
- **Complex choices** - multi-option decisions with visual trade-offs
|
||||
- **Design polish** - when the question is about look and feel
|
||||
- **Spatial relationships** - file structures, database schemas, state machines
|
||||
|
||||
**Always ask first:**
|
||||
> "This involves some visual decisions. Would you like me to show mockups in a browser window? (Requires opening a local URL)"
|
||||
|
||||
Only proceed if they agree. Otherwise, describe options in text.
|
||||
|
||||
### How to Use Effectively
|
||||
|
||||
**Scale fidelity to the question.** If you're asking about layout structure, simple wireframes suffice. If you're asking about visual polish, show polish. Match the mockup's detail level to what you're trying to learn.
|
||||
|
||||
**Explain the question on each page.** Don't just show options—state what decision you're seeking. "Which layout feels more professional?" not just "Pick one."
|
||||
|
||||
**Iterate before moving on.** If feedback changes the current screen, update it and show again. Validate that your changes address their feedback before proceeding to the next question.
|
||||
|
||||
**Limit choices to 2-4 options.** More gets overwhelming. If you have more alternatives, narrow them down first or group them.
|
||||
|
||||
**Use real content when it matters.** For a photography portfolio, use actual images (Unsplash). For a blog, use realistic text. Placeholder content obscures design issues.
|
||||
|
||||
### Starting a Session
|
||||
|
||||
```bash
|
||||
# Start server (creates unique 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"}
|
||||
```
|
||||
|
||||
Save `screen_dir` from the response. Tell user to open the URL.
|
||||
|
||||
### The Loop
|
||||
|
||||
1. **Start watcher first** (background bash) - avoids race condition:
|
||||
```bash
|
||||
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-feedback.sh $SCREEN_DIR
|
||||
```
|
||||
|
||||
2. **Write HTML** to a new file in `screen_dir`:
|
||||
- Use semantic filenames: `platform.html`, `visual-style.html`, `layout.html`
|
||||
- **Never reuse filenames** - each screen gets a fresh file
|
||||
- Use Write tool - **never use cat/heredoc** (dumps noise into terminal)
|
||||
- Server automatically serves the newest file
|
||||
|
||||
3. **Tell user what to expect:**
|
||||
- Remind them of the URL (every step, not just first)
|
||||
- Give a brief text summary of what's on screen (e.g., "Showing 3 layout options for the homepage")
|
||||
- This lets them know what to look for before switching to browser
|
||||
|
||||
4. **Wait for feedback** - call `TaskOutput(task_id, block=true, timeout=600000)`
|
||||
- If timeout, call TaskOutput again (watcher still running)
|
||||
- After 3 timeouts (30 min), say "Let me know when you want to continue"
|
||||
|
||||
5. **Process feedback** - returns JSON like `{"choice": "a", "feedback": "make header smaller"}`
|
||||
|
||||
6. **Iterate or advance** - if feedback changes current screen, write a new file (e.g., `layout-v2.html`). Only move to next question when current step is validated.
|
||||
|
||||
7. Repeat until done.
|
||||
|
||||
### Cleaning Up
|
||||
|
||||
```bash
|
||||
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR
|
||||
```
|
||||
|
||||
### Resources
|
||||
|
||||
- Frame template: `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/frame-template.html`
|
||||
- CSS classes: `.options`, `.cards`, `.mockup`, `.split`, `.pros-cons`
|
||||
- Detailed examples: `${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md`
|
||||
- Quick reference: `${CLAUDE_PLUGIN_ROOT}/skills/brainstorming/visual-companion.md`
|
||||
|
||||
130
skills/brainstorming/visual-companion.md
Normal file
130
skills/brainstorming/visual-companion.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Visual Companion Reference
|
||||
|
||||
Quick reference for the browser-based visual brainstorming companion.
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `lib/brainstorm-server/start-server.sh` | Start server, outputs JSON with URL and session paths |
|
||||
| `lib/brainstorm-server/stop-server.sh` | Stop server and clean up session directory |
|
||||
| `lib/brainstorm-server/wait-for-feedback.sh` | Wait for user feedback (polling-based) |
|
||||
| `lib/brainstorm-server/frame-template.html` | Base HTML template with CSS |
|
||||
| `lib/brainstorm-server/CLAUDE-INSTRUCTIONS.md` | Detailed usage guide |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Start server
|
||||
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/start-server.sh
|
||||
# Returns: {"screen_dir":"/tmp/brainstorm-xxx","url":"http://localhost:PORT"}
|
||||
|
||||
# 2. Start watcher FIRST (background bash) - avoids race condition
|
||||
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/wait-for-feedback.sh $SCREEN_DIR
|
||||
|
||||
# 3. Write HTML to a NEW file in screen_dir (e.g., platform.html, style.html)
|
||||
# Never reuse filenames - server serves newest file automatically
|
||||
|
||||
# 4. Call TaskOutput(task_id, block=true, timeout=600000)
|
||||
# If timeout, call again. After 3 timeouts (30 min), prompt user.
|
||||
# Returns: {"choice":"a","feedback":"user notes"}
|
||||
|
||||
# 5. Iterate or advance - write new file if feedback changes it (e.g., style-v2.html)
|
||||
|
||||
# 6. Clean up when done
|
||||
${CLAUDE_PLUGIN_ROOT}/lib/brainstorm-server/stop-server.sh $SCREEN_DIR
|
||||
```
|
||||
|
||||
## Key Principles
|
||||
|
||||
- **Always ask first** before starting visual companion
|
||||
- **Scale fidelity to the question** - wireframes for layout, polish for polish questions
|
||||
- **Explain the question** on each page - what decision are you seeking?
|
||||
- **Iterate before advancing** - if feedback changes current screen, write new version
|
||||
- **2-4 options max** per screen
|
||||
|
||||
## File Naming
|
||||
|
||||
- **Use semantic names**: `platform.html`, `visual-style.html`, `layout.html`, `controls.html`
|
||||
- **Never reuse filenames** - each screen is a new file
|
||||
- **For iterations**: append version suffix like `layout-v2.html`, `layout-v3.html`
|
||||
- Server automatically serves the newest file by modification time
|
||||
|
||||
## Terminal UX
|
||||
|
||||
- **Never use cat/heredoc for HTML** - dumps noise into terminal. Use Write tool instead.
|
||||
- **Remind user of URL** on every step, not just the first
|
||||
- **Give text summary** of what's on screen before they look (e.g., "Showing 3 API structure options")
|
||||
|
||||
## CSS Classes
|
||||
|
||||
### Options (A/B/C choices)
|
||||
```html
|
||||
<div class="options">
|
||||
<div class="option" data-choice="a" onclick="toggleSelect(this)">
|
||||
<div class="letter">A</div>
|
||||
<div class="content">
|
||||
<h3>Title</h3>
|
||||
<p>Description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Cards (visual designs)
|
||||
```html
|
||||
<div class="cards">
|
||||
<div class="card" data-choice="x" onclick="toggleSelect(this)">
|
||||
<div class="card-image"><!-- mockup --></div>
|
||||
<div class="card-body">
|
||||
<h3>Name</h3>
|
||||
<p>Description</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Mockup container
|
||||
```html
|
||||
<div class="mockup">
|
||||
<div class="mockup-header">Label</div>
|
||||
<div class="mockup-body"><!-- content --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Split view
|
||||
```html
|
||||
<div class="split">
|
||||
<div><!-- left --></div>
|
||||
<div><!-- right --></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Pros/Cons
|
||||
```html
|
||||
<div class="pros-cons">
|
||||
<div class="pros"><h4>Pros</h4><ul><li>...</li></ul></div>
|
||||
<div class="cons"><h4>Cons</h4><ul><li>...</li></ul></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Mock elements
|
||||
```html
|
||||
<div class="mock-nav">Nav items</div>
|
||||
<div class="mock-sidebar">Sidebar</div>
|
||||
<div class="mock-content">Content</div>
|
||||
<button class="mock-button">Button</button>
|
||||
<input class="mock-input" placeholder="Input">
|
||||
<div class="placeholder">Placeholder area</div>
|
||||
```
|
||||
|
||||
## User Feedback Format
|
||||
|
||||
```json
|
||||
{
|
||||
"choice": "option-id", // from data-choice attribute
|
||||
"feedback": "user notes" // from feedback textarea
|
||||
}
|
||||
```
|
||||
|
||||
Both fields are optional - user may select without notes, or send notes without selection.
|
||||
@@ -11,6 +11,16 @@ IF A SKILL APPLIES TO YOUR TASK, YOU DO NOT HAVE A CHOICE. YOU MUST USE IT.
|
||||
This is not negotiable. This is not optional. You cannot rationalize your way out of this.
|
||||
</EXTREMELY-IMPORTANT>
|
||||
|
||||
## Instruction Priority
|
||||
|
||||
Superpowers skills override default system prompt behavior, but **user instructions always take precedence**:
|
||||
|
||||
1. **User's explicit instructions** (CLAUDE.md, direct requests) — highest priority
|
||||
2. **Superpowers skills** — override default system behavior where they conflict
|
||||
3. **Default system prompt** — lowest priority
|
||||
|
||||
If CLAUDE.md says "don't use TDD" and a skill says "always use TDD," follow CLAUDE.md. The user is in control.
|
||||
|
||||
## How to Access Skills
|
||||
|
||||
**In Claude Code:** Use the `Skill` tool. When you invoke a skill, its content is loaded and presented to you—follow it directly. Never use the Read tool on skill files.
|
||||
|
||||
36
tests/brainstorm-server/package-lock.json
generated
Normal file
36
tests/brainstorm-server/package-lock.json
generated
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "brainstorm-server-tests",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "brainstorm-server-tests",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
tests/brainstorm-server/package.json
Normal file
10
tests/brainstorm-server/package.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "brainstorm-server-tests",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"test": "node server.test.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
}
|
||||
106
tests/brainstorm-server/server.test.js
Normal file
106
tests/brainstorm-server/server.test.js
Normal file
@@ -0,0 +1,106 @@
|
||||
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(300);
|
||||
|
||||
assert(stdout.includes('"source":"user-event"'), 'Should relay user events with source field');
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user