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

@@ -32,107 +32,56 @@
}
}
// Auto-capture clicks on interactive elements
// Capture clicks on choice elements
document.addEventListener('click', (e) => {
const target = e.target.closest('button, a, [data-choice], [role="button"], input[type="submit"]');
const target = e.target.closest('[data-choice]');
if (!target) return;
// Don't capture regular link navigation
if (target.tagName === 'A' && !target.dataset.choice) return;
// Don't capture the Send feedback button (handled by send())
if (target.id === 'send-feedback') return;
e.preventDefault();
sendEvent({
type: 'click',
text: target.textContent.trim(),
choice: target.dataset.choice || null,
id: target.id || null,
className: target.className || null
choice: target.dataset.choice,
id: target.id || null
});
// Update indicator bar (defer so toggleSelect runs first)
setTimeout(() => {
const indicator = document.getElementById('indicator-text');
if (!indicator) return;
const container = target.closest('.options') || target.closest('.cards');
const selected = container ? container.querySelectorAll('.selected') : [];
if (selected.length === 0) {
indicator.textContent = 'Click an option above, then return to the terminal';
} else if (selected.length === 1) {
const label = selected[0].querySelector('h3, .content h3, .card-body h3')?.textContent?.trim() || selected[0].dataset.choice;
indicator.innerHTML = '<span class="selected-text">' + label + ' selected</span> — return to terminal to continue';
} else {
indicator.innerHTML = '<span class="selected-text">' + selected.length + ' selected</span> — return to terminal to continue';
}
}, 0);
});
// 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; });
sendEvent({
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(() => {
sendEvent({
type: 'input',
name: target.name || null,
id: target.id || null,
value: target.value,
inputType: target.type || target.tagName.toLowerCase()
});
}, 500);
});
// Send to Claude - triggers feedback delivery
function sendToClaude(feedback) {
sendEvent({
type: 'send-to-claude',
feedback: feedback || null
});
// Show themed confirmation page
document.body.innerHTML = `
<div style="display: flex; align-items: center; justify-content: center; height: 100vh; font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; background: var(--bg-primary, #f5f5f7);">
<div style="text-align: center; color: var(--text-secondary, #86868b);">
<h2 style="color: var(--text-primary, #1d1d1f); margin-bottom: 0.5rem;">Sent to Claude</h2>
<p>Return to the terminal to see Claude's response.</p>
</div>
</div>
`;
}
// Frame UI: selection tracking and feedback send
// Frame UI: selection tracking
window.selectedChoice = null;
window.toggleSelect = function(el) {
const container = el.closest('.options') || el.closest('.cards');
if (container) {
const multi = container && container.dataset.multiselect !== undefined;
if (container && !multi) {
container.querySelectorAll('.option, .card').forEach(o => o.classList.remove('selected'));
}
el.classList.add('selected');
if (multi) {
el.classList.toggle('selected');
} else {
el.classList.add('selected');
}
window.selectedChoice = el.dataset.choice;
};
window.send = function() {
const feedbackEl = document.getElementById('feedback');
const feedback = feedbackEl ? feedbackEl.value.trim() : '';
const payload = {};
if (window.selectedChoice) payload.choice = window.selectedChoice;
if (feedback) payload.feedback = feedback;
if (Object.keys(payload).length === 0) return;
sendToClaude(payload);
if (feedbackEl) feedbackEl.value = '';
};
// Expose API for explicit use
window.brainstorm = {
send: sendEvent,
choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata }),
sendToClaude: sendToClaude
choice: (value, metadata = {}) => sendEvent({ type: 'choice', value, ...metadata })
};
connect();