Refactor visual brainstorming: browser displays, terminal commands (#509)

* Refactor visual brainstorming: browser displays, terminal commands

Replaces the blocking TaskOutput/wait-for-feedback.sh pattern with a
non-blocking model where the browser is an interactive display and the
terminal stays available for conversation.

Server changes:
- Write user click events to .events JSONL file (per-screen, cleared on
  new screen push) so Claude reads them on its next turn
- Replace regex-based wrapInFrame with <!-- CONTENT --> placeholder
- Add --foreground flag to start-server.sh for sandbox environments
- Harden startup with nohup/disown and liveness check

UI changes:
- Remove feedback footer (textarea + Send button)
- Add selection indicator bar ("Option X selected — return to terminal")
- Narrow click handler to [data-choice] elements only

Skill changes:
- Rewrite visual-companion.md for non-blocking loop
- Fix visual companion being skipped on Codex (no browser tools needed)
- Make visual companion offer a standalone question (one question rule)

Deletes wait-for-feedback.sh entirely.

* Add visual companion offer to brainstorming checklist for UX topics

The visual companion was a disconnected section at the bottom of SKILL.md
that agents never reached because it wasn't in the mandatory checklist.
Now step 2 evaluates whether the topic involves visual/UX decisions and
offers the companion if so. Non-visual topics (APIs, data models, etc.)
skip the step entirely.

* Add multi-select support to visual companion

Containers with data-multiselect allow toggling multiple selections.
Without it, behavior is unchanged (single-select). Indicator bar shows
count when multiple items are selected.
This commit is contained in:
Drew Ritter
2026-02-19 16:31:51 -08:00
committed by GitHub
parent 3a254ba002
commit ce0f9a28be
10 changed files with 960 additions and 207 deletions

View File

@@ -6,6 +6,8 @@ const fs = require('fs');
const path = require('path');
const PORT = process.env.BRAINSTORM_PORT || (49152 + Math.floor(Math.random() * 16383));
const HOST = process.env.BRAINSTORM_HOST || '127.0.0.1';
const URL_HOST = process.env.BRAINSTORM_URL_HOST || (HOST === '127.0.0.1' ? 'localhost' : HOST);
const SCREEN_DIR = process.env.BRAINSTORM_DIR || '/tmp/brainstorm';
if (!fs.existsSync(SCREEN_DIR)) {
@@ -25,10 +27,7 @@ function isFullDocument(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`
);
return frameTemplate.replace('<!-- CONTENT -->', content);
}
// Find the newest .html file in the directory by mtime
@@ -74,6 +73,11 @@ wss.on('connection', (ws) => {
ws.on('message', (data) => {
const event = JSON.parse(data.toString());
console.log(JSON.stringify({ source: 'user-event', ...event }));
// Write user events to .events file for Claude to read
if (event.choice) {
const eventsFile = path.join(SCREEN_DIR, '.events');
fs.appendFileSync(eventsFile, JSON.stringify(event) + '\n');
}
});
});
@@ -103,6 +107,9 @@ app.get('/', (req, res) => {
chokidar.watch(SCREEN_DIR, { ignoreInitial: true })
.on('add', (filePath) => {
if (filePath.endsWith('.html')) {
// Clear events from previous screen
const eventsFile = path.join(SCREEN_DIR, '.events');
if (fs.existsSync(eventsFile)) fs.unlinkSync(eventsFile);
console.log(JSON.stringify({ type: 'screen-added', file: filePath }));
clients.forEach(ws => {
if (ws.readyState === WebSocket.OPEN) {
@@ -122,11 +129,13 @@ chokidar.watch(SCREEN_DIR, { ignoreInitial: true })
}
});
server.listen(PORT, '127.0.0.1', () => {
server.listen(PORT, HOST, () => {
console.log(JSON.stringify({
type: 'server-started',
port: PORT,
url: `http://localhost:${PORT}`,
host: HOST,
url_host: URL_HOST,
url: `http://${URL_HOST}:${PORT}`,
screen_dir: SCREEN_DIR
}));
});