---
name: visual
description: Use when Pedro invokes `/visual` (with or without flags like `-auto-off`, `-auto-on`, `-status`), asks to "ver isso no HTML", wants a visual presentation of a plan/diagnostic/question, or when the PreToolUse hook `pre-exitplan-visualize.sh` on ExitPlanMode blocks. ALSO invoke PROACTIVELY when auto mode is on (read the current value in `${CLAUDE_PLUGIN_ROOT}/skills/visual/config.json`) and you're about to emit a plan with 3+ items, a decision with 2+ options, a diagnostic with 3+ problems, or a long explanation (40+ lines / 3+ sections). The page MUST carry the evidence, not the account of it — the real artifact embedded, the raw output verbatim, the provenance of every number. Generates a dark-theme HTML inside the project's `.claude/visual/` (falls back to `~/Desktop/claude-visual/` outside a project), spawns a local daemon for live-sync back to Claude (Pedro types "ok" and Claude reads state from disk — no copy/paste), and opens it in the browser. Replaces 20-page CLI dumps with scannable visual surfaces — artifact and evidence first, decision on top of the fold, technical details collapsed below.
---

# Skill: /visual

Turn walls of CLI text into a scannable HTML surface. Dark theme, opens in browser.

## Mostrar, não descrever — a regra que manda em todas as outras

A página **carrega a prova, não o relato da prova.** O Pedro tem que ver na própria página as mesmas coisas que você olhou quando formou a conclusão. Nas palavras dele:

> "Eu preciso ver **o que você viu** que te fez pensar o que você pensou e que está te fazendo me pedir para decidir uma coisa. Se você não me mostrar — não é me explicar, nem me descrever, é me **MOSTRAR** o que você viu durante a tua análise — eu não tenho como fazer nada."

**Referente pendurado é o defeito.** Toda vez que a página se apoia em algo — uma coisa que existe, uma coisa em discussão, uma premissa, uma conclusão anterior, "o approach atual" — esse algo é um ponteiro. Ponteiro sem alvo na página é pedir bola de cristal. Três classes e o que "mostrar" significa em cada uma:

- **Coisa que existe** (slide, tela, deck, arquivo, código) → embutida (`.artefato` + `<iframe src="file:///caminho/real">`) ou citada literal com `arquivo:linha`. Testado: caminho absoluto cross-dir renderiza no Chrome.
- **Número ou afirmação medida** → o output cru que a produziu, verbatim, em `.evidencia`, com o comando que gerou. Não "medi e deu +155%": as linhas que você leu, com n, com as duas amostras.
- **Premissa, conclusão anterior, coisa em discussão** → a passagem literal (quem disse, onde), o commit, o trecho do plano. Sem fonte no disco, rotule **INFERIDO** — nunca apresente como fato.

### A forma da página de decisão

Dez linhas de leitura até a decisão. Nessa ordem:

1. **Faixa de identidade** (`.ident-strip`) — projeto, artefato **pelo título visível** (nunca por id interno), e o **estado** dele: rascunho, gerado, no ar, já apresentado.
2. **O artefato**, embutido.
3. **4-5 bullets** — do que se trata e o que ele afirma.
4. **A evidência crua** (`.evidencia`) — com procedência: comando · arquivo · quando.
5. **2-3 bullets** — qual é o problema.
6. **A decisão**.

O aprofundamento vai em `<details>`: disponível, não empurrado. Se ele quiser mais, pede.

**Não é concisão, é especificidade.** Os dois extremos reprovam igual: o textão que enterra o insumo, e o lacônico que não deixa decidir. O critério é o insumo crucial da decisão estar lá, e nada além.

### Ordem de construção: pergunta primeiro, prova filtrada depois

A causa da recaída é a ordem. Você termina a análise, tem a conclusão na cabeça, e escreve a página de trás pra frente — a evidência fica no scrollback e parece "trabalho já feito", então vira resumo.

Inverta: **escreva a pergunta da decisão antes de qualquer coisa**, liste o que muda a resposta dela, e só isso entra na página. Se não há prova pra colar, não há decisão a pedir — há investigação a fazer.

### Proibido fabricar o "depois"

Se a versão corrigida não existe, **não monte uma** por find-and-replace, mock-up ou edição manual. Mostre o artefato **real de hoje** + a medição do que seria, rotulada como **sua medição**, nunca como saída do motor/sistema. Artefato fabricado ao lado do real é indistinguível pra quem lê.

### Afirmação que cria pressa tem que ser checada

"Já foi apresentado ao cliente", "está no ar", "o cliente viu", "isso quebra em produção" — toda afirmação que cria urgência enquadra a decisão inteira. Cheque no disco **nesta sessão** antes de escrever, ou rotule INFERIDO. No caso que originou esta regra, a afirmação era falsa e era ela que criava a urgência.

### Autoteste antes de abrir o browser

Releia como se nunca tivesse visto a conversa:

1. **Cadê o artefato?** A coisa de que a página fala está embutida ou citada literal? "Eu descrevi" reprova.
2. **Cadê a saída?** Todo número aponta pra um bloco de evidência **nesta mesma página**? Número sem origem reprova.
3. **Cadê o endereço?** A página nomeia projeto, artefato pelo título visível, e o estado dele?
4. **A urgência foi checada?** Toda afirmação que cria pressa foi verificada nesta sessão?
5. **Dá pra decidir sem jargão?** Você consegue dizer o que aprova, por quê, e o que cada caminho custa — só pelo card?

O teste de aceitação real: se a primeira reação do Pedro for **"do que você está falando?"**, "onde?", "qual?" ou "me mostra" — a página falhou, por melhor escrita que esteja.

## When to invoke

- **Explicit**: Pedro types `/visual`, says "faz visual disso", "preciso ver no HTML", "joga pro browser".
- **Toggle commands** (handled inside this skill, NOT regenerating):
  - `/visual -auto-off` — disables auto mode. Writes `auto_mode: false` to config.json.
  - `/visual -auto-on` — re-enables auto mode. Writes `auto_mode: true`.
  - `/visual -status` — prints current config and returns.
  When Pedro passes any of these flags, DO NOT generate a visual — just update config and confirm in CLI.
- **Auto mode** — **leia o `config.json`, não presuma o valor** (o default de fábrica é `true`, mas o Pedro liga e desliga; hoje pode estar `false`). Se `${CLAUDE_PLUGIN_ROOT}/skills/visual/config.json` tem `auto_mode: true`, invoque esta skill sozinho sempre que for emitir:
  - A plan (numbered items, 3+ steps)
  - A decision with ≥2 options
  - A diagnostic with 3+ problems
  - A long explanation (≥40 lines of prose or ≥3 sections)
  Threshold numbers come from `auto_triggers` in config.json. Pedro can tune them by editing the file directly or by saying "aumenta o threshold pra X".
- **Auto-triggered (gate-blocked)**: the PreToolUse hook `pre-exitplan-visualize.sh` on `ExitPlanMode` blocks with exit code 2 and tells you to render the plan as HTML before the plan is shown to Pedro. Pedro wants to READ in the browser, not the CLI. This hook fires regardless of auto mode (plan mode is always visualized).
- **Explicit override when auto is off**: if `auto_mode: false`, wait for Pedro to type `/visual` or ask explicitly. Do NOT proactively generate.

**Decision tree at start of skill invocation:**

```
1. Read ${CLAUDE_PLUGIN_ROOT}/skills/visual/config.json
2. Parse args from the /visual command:
   - If "-auto-off" → update config, write, confirm, STOP.
   - If "-auto-on"  → update config, write, confirm, STOP.
   - If "-status"   → print config, STOP.
3. Otherwise → generate visual as normal.
```

## What to render

Detect the content type from the last substantial message or plan file:

| Content type | Shape |
|---|---|
| Plan (numbered items, exec summary) | Decision card + plan items as `<details>` + exec summary at bottom |
| Diagnostic (problems + what's working) | Problems grid on top (severity colors), "funcionando" section below |
| Question with options | Decision card with selectable cards (A/B/C), recommendation highlighted |
| Mixed / generic | Hero + sections + exec summary, following same hierarchy |

## Output location

The visual is saved **inside the project**, not on the Desktop. The target directory is decided by a 3-level cascade (stops at the first that matches):

1. **Git root** — if the cwd is inside a git repo → `<repo-root>/.claude/visual/`
2. **Project marker** — else, walking up from the cwd (stopping before `$HOME`), the first dir holding `package.json` / `CLAUDE.md` / `pyproject.toml` / `Cargo.toml` / `go.mod` / `graphify-out/` / `.git` → `<dir>/.claude/visual/`
3. **Desktop fallback** — nothing found (e.g. running loose in a non-project dir or `$HOME`) → `~/Desktop/claude-visual/`

**Do not hardcode the directory.** Run the resolver and use its stdout:

```
bash ${CLAUDE_PLUGIN_ROOT}/skills/visual/resolve-dir.sh "$PWD"
```

It prints the absolute target dir and creates it (`mkdir -p`). The filename pattern is:

```
<resolved-dir>/YYYY-MM-DD-sess-<session8>-<slug>.html
```

**Session-scoped naming is mandatory when invoked by the pre-ExitPlanMode hook.** The `<session8>` is the first 8 characters of the Claude Code `session_id` that the hook passes to you via stderr. The hook finds the current session's visual by matching `*sess-<session8>*.html` **in the same resolved dir** — if the filename doesn't contain that token, the hook won't recognize it and will block again. When the hook blocks, it already prints the full resolved path in stderr — save exactly there.

For manual invocation (Pedro types `/visual` outside plan mode), session scoping is optional; a simpler `YYYY-MM-DD-<slug>.html` inside the resolved dir is fine.

Slug = kebab-case of main topic (e.g., `plan`, `diagnostico-cron`, `decisao-arquitetura`).

After writing, always run `open <path>` to show it.

## Hierarchy rules (non-negotiable)

0. **Antes de tudo: de que se trata.** A faixa de identidade + o artefato + a prova vêm antes da decisão. O mecanismo do erro é vívido pra quem acabou de investigar e **inexistente** pra quem vai decidir — nunca abra por ele. A decisão continua dominante, logo abaixo, não no fim da página.
1. **Top**: the decision Pedro must make. Max 1 main decision per HTML. Large, visually dominant. Every decision carries its own plain-language context line (`.decision-context`) — see "O pedido de aprovação tem que se explicar sozinho" below.
2. **Middle**: context + justification. 3-5 bullets max. Link to concrete data (friction counts, real metrics, file paths) when available. This is section-level background — it does NOT replace the per-decision context line. Both exist: the `.decision-context` says what's at stake for THAT decision; the middle bullets give the broader why.
3. **Bottom**: technical detail in `<details>` collapsed by default. User expands only if they want depth.
4. **Reviewable items carry their verdict INLINE**: every item Pedro decides on (plan step, benchmark finding, proposed feature) is a `.feedback-item` with its keep/change/remove radios in the item's own header — NOT re-listed in a separate box at the bottom. See "Feedback channel" below. This is non-negotiable: the old "second table of approval" is a forbidden anti-pattern.
5. **Before feedback**: executive summary with 🔧/💡/📁 labels per item.
6. **After `.exec` (when `.decision-card` is present)**: the **Decisions box** — live summary + comments + copy button. Mandatory whenever decision cards exist. See "Decisions channel" below.
7. **After `.exec` (when reviewable items exist)**: the **Feedback box is a CLOSING box only** — progress bar + general observation + the two action buttons. It NEVER re-lists the items. See "Feedback channel" below.
8. **When both exist**: decisions-box comes first (right after `.exec`), feedback-box is last. The CSS `:has()` rule automatically demotes the decisions-box sticky-actions to inline so the two sticky bars don't collide.

If there's no decision pending, skip the decision block — don't fake one.

## O pedido de aprovação tem que se explicar sozinho (non-negotiable)

This skill governs the *container* well (where boxes go, copy semantics, sync). This section governs the thing that was missing: **the language and self-sufficiency of what you're asking Pedro to approve.** The mechanics being perfect is worthless if Pedro reads the card and still can't tell what he's signing off on.

This is the most-violated rule. Read it before writing any decision, approval item, plan title, finding, or exec line.

### The reader test (altitude)

Pedro reads **only the HTML**. Never assume he read the CLI, the plan file, or the code — those don't exist for him at the moment of deciding. After reading a card, if he can't say **in his own words** (a) what he's approving, (b) why he's being asked, and (c) what changes with each choice — the card failed. Rewrite it.

### Every decision / approval item carries three things, in its own body

1. **O quê** — what's being approved or decided. One plain line. This is the title or the `.decision-q`.
2. **Por quê / a premissa** — what prompted it, what's at stake. 1-2 sentences. For a decision card this is the `.decision-context` line (see below). For a plan item it's the first line of the body.
3. **A consequência de cada opção** — what each choice actually causes. One plain line per option (the option's `<p>` or `.tradeoff`). "✔ Robusto · ✘ Polui Desktop" is a consequence; "Opção A" is not.

This mirrors Pedro's `perguntas-autocontidas` memory: every decision carries its premises and consequences **in its own body**. A bare label is rejected.

### Human language — banned vocabulary

The text Pedro reads is for a human, not a log. Forbidden as the visible headline/title/option of what he approves:

- **Code identifiers** as the thing on display: `data-num`, `latest.json`, `postState()`, `state-change`, class names, function names.
- **Internal jargon / category codes**: `wrong_approach`, `buggy_code`, `over-planning`, error codes, enum values, ticket slugs.
- **Agentic / process talk**: "injeta o script após o body", "o parser detecta o marker", "o hook dispara exit 2", "fetch POST pro daemon". Pedro doesn't approve your process — he approves an outcome.

**Exceção que manda nesta seção: output cru dentro de `.evidencia` NÃO é jargão — é prova.** Esta regra proíbe jargão **no lugar da** prova, nunca a prova em si. Cole a saída literal do terminal, o stack trace, as linhas da query, o trecho de código com `arquivo:linha` — sem parafrasear, sem "humanizar", sem resumir. O que tem que estar em linguagem humana é o **título, a pergunta e as consequências**; o insumo é cru por obrigação.

Allowed: a path or command as **secondary** detail inside `code.inline` (e.g. "salvo em `~/Desktop/...`") — never as the title of what's being approved. If a technical term is genuinely unavoidable, gloss it in one plain phrase right after: "o daemon (o programinha que sincroniza em segundo plano)".

This mirrors Pedro's `sem-jargao-proprio` memory and the CLAUDE.md rule: *"Problemas devem ser explicados em 1-2 linhas, em linguagem humana e intuitiva"* — never "somente da forma técnica".

### The two failure modes — and the ruler between them

There's a chasm between two bad extremes, and the skill keeps falling into one or the other:

- **❌ The wall** — a giant paragraph re-explaining everything from zero, every caveat, the whole history. Pedro can't find the decision inside it.
- **❌ The bare label** — so terse he can't act: "Aprovar refactor?", "Opção A / B / C", a title that's just a code name.

The ruler: **enough to decide, nothing more.** Concretely — question in one plain line, premise in 1-2 sentences, one consequence line per option. If you wrote three paragraphs, cut to the premise. If you wrote four words, you owe the premise and the consequences.

### Self-check before opening the browser

Reread each decision/approval block as if you'd never seen the conversation. Can you state what it approves, why, and what each path costs — from the card alone, with zero jargon? If not, it's not ready to show.

## Decisions channel (for questions/decisions — non-negotiable when decision cards are present)

**Context:** when the HTML contains `.decision-card` blocks (main decision or sub-decisions), Pedro clicks to pick options but that state stays inside the browser. Without a feedback channel, Claude has no way to know what he chose — Pedro would have to retype each answer in the CLI. This component bridges that gap.

**When to include `.decisions-box`:** always, when the HTML has at least one `.decision-card`. Mandatory. This applies to "question with options" content, plans with sub-decisions, any rendering that includes option cards.

**Three components inside `.decisions-box`:**

1. **Live summary panel** (`.dsum-list` with `id="dsum-list"`) — updates in real time as Pedro clicks option cards. Shows each decision's question + the chosen option title (with custom note if any). This is the "tail" — Pedro sees his current state reflected back immediately without leaving the page.
2. **Free-form comments textarea** (`#dec-comments`) — for Pedro to jot open-ended thoughts, next-steps, things to discuss after. Not tied to any specific decision.
3. **Copy button** (`btn.btn-primary` with `onclick="copyDecisions(this)"`) — collects every selection + comments into a markdown-formatted block on the clipboard. Pedro pastes in the CLI so Claude knows what he chose.

**O HTML está no `template.html`, não aqui.** Copie o bloco `.decisions-box` do template — não reescreva de memória e não copie de nenhuma versão colada em prosa. (Havia uma cópia nesta skill que já tinha divergido: faltava o `.live-indicator`, então quem a seguisse entregava a página sem o selo de sync.)

**Where to place it:** AFTER the `.exec` summary. If the HTML also has `.feedback-box`, decisions-box goes BEFORE feedback-box (feedback-box stays last because plan-review takes priority; decisions-box's sticky automatically becomes inline via CSS `:has()` rule when feedback-box is present).

**Copy output format (versioned, v1):**

The `copyDecisions` function emits a markdown block wrapped in versioned HTML comments:

```
<!-- visual-decisions v1 -->
📋 **Minhas escolhas:**

- **Decisão 1**: GitHub público · Como hospedar o marketplace?
- **Decisão 2**: Personalizada · Agrupamento
  - _Nota_: 2 plugins (handoff+wrapup juntos, carregar-handoff separado)

**Comentários / próximos passos:**
Quero pensar melhor na decisão 3.

<!-- /visual-decisions -->
```

Claude SHOULD detect the `<!-- visual-decisions v1 -->` marker when parsing pasted content — this is the signal that the user pasted a decisions block. Future format evolutions bump the version (`v2`, etc.) without breaking old-version detection.

Já implementado no template (não reescreva): aviso de `confirm()` quando ele copia com decisões faltando, e persistência em `localStorage` por arquivo (`claude-visual:<pathname>`), pra refresh ou fechada acidental não apagar o progresso.

## Live sync via `claude-visual-server`

O copy/paste é o fallback. O caminho normal é o daemon: o Pedro mexe no browser, o daemon escreve em disco, o Claude lê quando ele diz "ok" / "pronto" / "lido".

**Files:**
- `${CLAUDE_PLUGIN_ROOT}/server/visual_server.mjs` — the daemon (Node stdlib only, zero deps). Binds `127.0.0.1:7755`.
- `${CLAUDE_PLUGIN_ROOT}/server/start.sh` — idempotent starter. Pings the port; if nothing responds, spawns `node visual_server.mjs` detached. Soft-fails if Node is missing.
- `~/.claude/visual-state/<session>.json` — per-session state file (rewritten on every POST from browser).
- `~/.claude/visual-state/latest.json` — always points to the most recently updated session. **Claude reads THIS file** to fetch the user's current state without needing to know the token.

**Endpoints:**
- `GET  /ping` — liveness probe (returns `{status,pid,port}`).
- `POST /state` — body `{session, docTitle?, state}`. Writes `<session>.json` + updates `latest.json`.
- `GET  /state?session=<id>` — reads a specific session (debugging).

**Skill workflow when generating an HTML (mandatory):**

1. Generate a unique session token. Format: short, matching `^[a-zA-Z0-9_-]{4,64}$`. Recommended: `<YYYYMMDDHHMM>-<rand6>` e.g. `202604201230-a3f2k9`.
2. Right after `<body>`, inject:
   ```html
   <script>window.VISUAL_SESSION = "<token>";</script>
   ```
3. Before `open "<path>"`, run `${CLAUDE_PLUGIN_ROOT}/server/start.sh`. Idempotent — if daemon already running, no-op.
4. Open the HTML.

**Behavior in the browser:**
- Every `saveState()` call (triggered on every click, key press, option-card change) also calls `postState()`, debounced at 400ms.
- `postState()` does `fetch POST http://127.0.0.1:7755/state` with `{session, docTitle, state}`.
- On success, the `.live-indicator` pill in the decisions-box turns green (`live sync`). On failure (daemon off), it turns amber (`copy manual`) — copy/paste button still works.

**How Claude reads state (the "tail" part):**

When Pedro signals completion via short triggers like **"ok"**, **"pronto"**, **"lido"**, **"tá bom"**, **"finalizei"**, Claude MUST:
1. Check `~/.claude/visual-state/latest.json` exists.
2. Read it, parse `state` field.
3. Act on the state the same way it would parse a pasted markdown block — decisions, comments, feedback items, general notes.
4. Respond.

If `latest.json` doesn't exist or is stale (>30min old), Claude falls back to asking Pedro to paste the copy/paste block.

**Security surface (deliberate):**
- Daemon binds **only** `127.0.0.1` (never `0.0.0.0`) — unreachable from the network.
- Session token is validated against `^[a-zA-Z0-9_-]{4,64}$` — no path traversal possible.
- Max body size 256KB — no DoS via huge uploads.
- 30-minute idle shutdown — daemon doesn't linger as zombie.
- CORS is `*` (needed for `file://` contexts which send origin `null`). Acceptable since daemon only binds local.

**Auto-shutdown:** daemon kills itself after 30 min of no requests. Next `/visual` invocation respawns.

**Graceful degradation:** HTMLs generated without `window.VISUAL_SESSION` (old files, or Pedro opens template.html directly) never try to sync — `postState()` is a no-op when the session is absent. Backward compatible.

## Feedback channel (verdict INLINE per item — the most important part)

Os controles de veredito por item são o canal canônico de feedback do Pedro pro Claude.

### The verdict lives ON the item — never in a second table (non-negotiable)

**Por quê:** listar o conteúdo e depois re-listar tudo num menu de aprovação no fim obriga o Pedro a rolar de volta (ou guardar de cabeça) o que era o item #1 quando decide o #50. Ele decide **enquanto lê**.

**Rule:** each reviewable item is a single `.feedback-item` that **contains** its own content. The keep/change/remove radios sit in the item's `.feedback-head` (right next to the title), and the depth goes in a `<details class="item-detail">` inside the same item. Pedro marks his verdict while reading the item. **Re-listing items with controls in a separate block at the bottom is a forbidden anti-pattern** — that is the "two tables" bug. There is exactly ONE list.

**When to render inline verdicts:** any list where Pedro decides item-by-item — plan tasks, benchmark/report findings, proposed features, schema choices. A purely informational diagnostic (nothing to approve/reject) gets no verdict controls.

**How to build each item:**

- **One `.feedback-item` per reviewable item**, wrapping its content. `data-num` + `data-title` carry the identity (e.g. `data-num="1" data-title="Onboarding guiado"`).
- The `.feedback-head` holds: `.feedback-num` + `.feedback-title` + the three radios. Radios go in the head, **outside** the `<details>` — this avoids the click-on-radio-toggles-the-details conflict without any `preventDefault` hack.
- **Machine values are fixed: `keep` / `change` / `remove`** (the clipboard parser and live-sync depend on them). The visible label text adapts to the content: a plan → "✓ Manter / ✏️ Mudar / ✗ Remover"; findings or features → "✓ Aprovar / ✏️ Ajustar / ✗ Negar". Same values, humanized wording.
- Radio `name` must be unique per item (`fb-1`, `fb-2`, …).
- **NO pre-selected verdict (non-negotiable, Pedro 2026-06-20).** Never ship a radio with `checked` and never seed the item with `state-keep`. The item must render with NOTHING marked, so Pedro has to actively decide — *especially on implementation items*. A pre-checked `<input checked>` ALSO never fires `onchange` on load, so the progress counter would read 0 while the radio looks selected — the exact "parece marcado mas não conta, tenho que mudar e voltar" bug. Untouched items copy as "⚠️ sem veredito" (only **Aprovar tudo** treats untouched as ok).
- Depth goes in `<details class="item-detail">` with `.read-dot` + summary + `.dchev` + `.detail-body`. Optional `.sev` severity tag (`sev-high`/`sev-med`/`sev-low`) next to the title for findings.
- When Pedro picks "Mudar/Ajustar", the textarea appears (pure CSS via `.feedback-item.state-change .feedback-textarea { display: block }`). The radios also recolor per state (change → warn, remove → danger).

**The closing `.feedback-box` is NOT a second list.** It holds only: the progress bar (`X/N itens revisados`), the general-observation textarea (`#fb-general`), and the two action buttons. It must NEVER re-render the items. Title it "🏁 Fechamento", not "Seu feedback".

**Action buttons:** **"Aprovar tudo"** and **"Copiar feedback"**. Both — together with **"Copiar escolhas"** in the decisions-box — emit the **full user-input state** (decisions + dec-comments + feedback items + fb-general). They differ only in the leading verdict and trailing action cue, never in what they capture.

### Non-siloed copy semantics (mandatory)

Nas palavras do Pedro: *"o botao final tem que resumir tudo qeu rolou ao longo do caminho"*. Botão que captura só a própria caixa faz sumir comentário que ele escreveu em outro lugar.

**Rule:** every copy button calls `collectAllInput()` which concatenates decisions + `#dec-comments` + feedback items (with per-item `change` notes) + `#fb-general`. The buttons then wrap this body with a button-specific envelope:

| Button | Leading envelope | Trailing cue | Extra |
|---|---|---|---|
| `copyDecisions` ("Copiar escolhas") | `<!-- visual-decisions v1 -->` + `📋 Snapshot — escolhas e comentários` | `<!-- /visual-decisions -->` | confirm dialog if selections incomplete |
| `approveAll` ("Aprovar tudo") | `<!-- visual-approve v1 -->` + `✅ APROVADO — <title>` | `Pode prosseguir.` + `<!-- /visual-approve -->` | confirm dialog if any feedback item is `change`/`remove` (prevents accidental approval while asking for changes) |
| `copyFeedback` ("Copiar feedback") | `<!-- visual-feedback v1 -->` + `📝 Feedback no plano` | `Ajuste o plano e mostre de novo.` OR `Tudo certo — pode implementar.` + `<!-- /visual-feedback -->` | — |

**Parser implications for Claude:** the three markers (`visual-decisions`, `visual-approve`, `visual-feedback`) signal intent (snapshot vs approval vs rework), but the **body structure is identical**. When parsing pasted content, Claude should extract decisions + feedback together regardless of which marker wrapped them. The marker tells Claude what Pedro wants done with the information, not what fields to look for.

**How Pedro uses it:**

1. Plan mode prompt appears in the CLI with the plan.
2. Pedro reads it in the HTML (already open in browser).
3. Pedro fills out decisions + feedback + any comments in the HTML.
4. Pedro clicks whichever button matches his verdict: "Aprovar tudo" (approves, includes his observations), "Copiar feedback" (asks for changes, includes full state), or "Copiar escolhas" (snapshot at any point, full state).
5. Pedro goes to the CLI, pastes. Claude reads the marker + body and acts.

**Never** dump the plan text into the CLI response. The HTML IS the plan view. The CLI just handles the accept/reject mechanical step.

## Template

Use this as the base. Customize content, not the CSS or structure.

The canonical template lives at **`${CLAUDE_PLUGIN_ROOT}/skills/visual/template.html`**.

Read that file. It is a fully-working **Variant B** (indigo background + peach accent + rounded cards) self-contained HTML. Components included:

- **`.ident-strip`** — faixa de identidade no topo: projeto · artefato pelo título visível · o que gerou · `.estado` (`estado-rascunho`/`estado-gerado`/`estado-noar`/`estado-apresentado`). **Obrigatória em toda página que fala de algo que existe.**
- **`.artefato`** — moldura do artefato real: `.artefato-bar` (selo "artefato real" + procedência) + `<iframe src="file:///…">` ou `srcdoc` ou `<img>`. Nunca uma versão fabricada.
- **`.evidencia`** — a prova crua: `.evidencia-src` (comando · projeto · quando) + `<pre>` com scroll próprio e `max-height`. Use `<mark>` pra destacar a linha que decide. A variante `.evidencia.vazio` grita na tela quando não há prova colada — e o hook bloqueia antes de abrir.
- **`.bullets`** / **`.bullets.problema`** — os 4-5 bullets do que se trata e os 2-3 do problema.
- **`.diagram`** — container de SVG/ASCII inline (com `figcaption`/`.cap` opcional)
- **`.opt-illustration`** — SVG dentro do card de opção (viewBox `0 0 100 60`, 64px de altura)
- **`.pill`** — label tags (kicker, decision-label, etc.)
- **`.tldr`** — one-sentence summary card with emoji
- **`h1`** + **`.subtitle`** — hero
- **`.meta-chips`** — reading-cost chips (time, items, decisions, date)
- **`.decision-card`** — wraps 3 option cards
- **`.decision-context`** — mandatory plain-language line right below `.decision-q`. One or two sentences saying what's at stake / what prompted the question, in human language (no code, no jargon). See "O pedido de aprovação tem que se explicar sozinho". Required on every decision card.
- **`.opt`** — option card (emoji + title + tradeoff). 3 per decision. 3rd is always `.opt-custom` with embedded `.opt-custom-input` textarea. Each option's `<p>`/`.tradeoff` states the consequence of picking it, in plain words.
- **`.feedback-item`** — the unified reviewable item: `.feedback-head` (num + title + keep/change/remove radios) + `.item-detail` (`<details>` with `.read-dot`/`.dchev`/`.detail-body` for depth) + inline `.feedback-textarea`. This is what carries the INLINE verdict. Use it for every plan step / finding / feature Pedro decides on. Demo: section 2 of the template.
- **`.sev`** — optional severity tag (`sev-high`/`sev-med`/`sev-low`) shown next to a finding's title
- **`.plan-item`** (`<details>`) — legacy collapsible block with `.read-dot` + `.plan-num` + `.plan-title`, for NON-reviewable read-only content only. For anything Pedro decides on, use `.feedback-item` instead.
- **`.card-tile`** — friction/metric grid cell
- **`.callout`** — side-ruled note (info/warn/danger/ok variants)
- **`.exec`** — exec summary card
- **`.feedback-box`** — CLOSING box only: progress + general observation + sticky action buttons. NEVER re-lists items (the verdicts live inline on each `.feedback-item`).
- **`.decisions-box`** — live summary of selected options + comments + copy button (mandatory when `.decision-card` is present)
- **`.sticky-actions`** — copy-feedback bar glued to bottom
- **`prefers-reduced-motion`** respected

To render a new visual: copy `template.html` to the target path, keep ALL CSS and JS untouched, replace the content inside `<body>` with the current plan/diagnostic/question. Do not invent new class names.


## Ilustrar — mas ilustração não substitui prova

Pedro lê imagem muito mais rápido que texto. Conceito com ≥3 entidades ligadas (arquitetura, fluxo, antes/depois, árvore, quem-chama-quem) vai ilustrado, não em prosa densa.

**Atenção à ordem:** o diagrama explica o **mecanismo**; a `.evidencia` mostra o **fato**. Diagrama bonito no lugar da saída crua é o defeito original com outra roupa. Primeiro a prova, depois o desenho que a explica.

**Como, em ordem de preferência:**

1. **SVG inline** dentro de `.diagram` — `viewBox`, `stroke="currentColor"`, formas simples, `<marker>` pra seta, traço 1.5–2px.
2. **ASCII** dentro de `<pre class="raw">` — pra fluxo e árvore quando SVG for exagero.
3. **Emoji em sequência** — micro-ilustração no meio do texto (`📥 input → 🔍 parse → 📤 output`).

**Imagens e artefatos — nada de rede, mas disco local pode:**

Nunca `<img src="https://...">`. A página não pode depender da internet.

- **Imagem** (screenshot, foto, figura de doc) → base64 inline: `base64 < image.png` → `<img src="data:image/png;base64,...">`. Imagem grande incha o arquivo; prefira recriar em SVG quando der.
- **Artefato que vive no disco** (o HTML de um slide, um PDF, uma página gerada) → `<iframe src="file:///caminho/absoluto">` dentro de `.artefato`. **Testado no Chrome:** renderiza, inclusive cross-dir com caminho absoluto; arquivo ausente vira caixa cinza (falha visível, não silenciosa).

Isso relaxa de propósito o "self-contained" antigo, e o teto é conhecido: o `iframe` mostra o arquivo **de agora**, não o de quando a decisão foi tomada, e quebra se o arquivo sumir. Aceito — a página de decisão vive horas, não anos, e apontar pro arquivo real torna impossível fabricar o "depois". Se o artefato for pequeno e a página precisar virar registro histórico, embuta uma cópia (`srcdoc` ou `<pre>`) em vez de referenciar.

**The rule is:** if you're rendering dense text and you thought for even a second "this could be a diagram", it SHOULD be a diagram.


## Multiple sub-decisions — each one gets its own option cards

**Rule:** if the content describes multiple sub-decisions Pedro has to make (e.g., "8 questions the plan needs to answer", "schema choices", "per-module configuration"), EACH sub-decision gets its own mini decision block with 3 option cards (including the mandatory `.opt.opt-custom` third card). Never reduce a decision to "my inclination" as read-only text.

**Detection heuristic:** if a plan section contains N items and each has a question mark, branching language ("ou", "entre X e Y"), or phrases like "decidir se", "escolher entre", "tag única vs múltipla" — that's N decisions, not N plan items.

**Rendering pattern for N sub-decisions:**

```html
<section>
  <div class="section-head">
    <span class="section-num">2</span>
    <h2>Sub-decisões</h2>
  </div>

  <!-- Repeat this block per sub-decision -->
  <div class="decision-card">
    <div class="pill">⚡ Sub-decisão · Pergunta 1 de 8</div>
    <h3 class="decision-q">Modalidades — enum fixo ou livre?</h3>
    <div class="options" role="radiogroup">
      <div class="opt">...A...</div>
      <div class="opt recommended">...B (com minha inclinação marcada)...</div>
      <div class="opt opt-custom">...C com textarea...</div>
    </div>
  </div>
  <!-- ...next sub-decision... -->
</section>
```

My inclination still shows — but as the `.recommended` card, not as the only answer. Pedro picks card or writes in "Outra".

**Inline-verdict mapping:** the N sub-decisions are option-card blocks (Pedro picks A/B/C inline, tracked by the decisions-box). If on top of that the plan also has reviewable tasks, those tasks are `.feedback-item`s with inline keep/change/remove — never re-listed in the closing box. The copy output includes both the picked options and the per-task verdict.


## Regras que vieram de bug (o conserto já está no CSS/JS do template)

- **Nada nasce pré-selecionado.** Zero `checked`, zero `state-keep`. Radio pré-marcado nunca dispara `onchange` no load: parece escolhido e o contador lê 0. A decisão é do humano; sugestão é badge, nunca resposta pré-preenchida.
- **`.recommended` é sugestão, não destaque permanente.** Assim que o Pedro escolhe, a recomendação sai da frente (só o badge ★ fica) pra a escolha real ser inequívoca.
- **Todo card de opção é clicável** — `role="radio"`, `tabindex="0"`, `onclick`, estado `.selected` com ✓.

## Option cards (decision block) — always 3 with illustrations

When you render a decision block with option cards, there are **two non-negotiable rules**:

### 1. Always include a third "Outra — eu especifico" card

Every options grid has exactly 3 cards: two concrete proposals (Opção A, Opção B) plus a third `.opt.opt-custom` card labeled **"Outra — Eu especifico"**. Pedro uses the embedded `<textarea class="opt-custom-input">` to write his own alternative when none of the pre-baked options fit. The textarea only reveals when the custom card is selected (handled by CSS: `.opt.opt-custom.selected .opt-custom-input { display: block }`), and uses `event.stopPropagation()` on click/keydown/input so typing in the textarea doesn't bubble up and mess with the card's selection state.

Never ship a decision card with only 2 options. Pedro must always have the escape hatch.

### 2. Illustrate options with inline SVG when possible

Cada `.opt` começa com `<span class="opt-illustration">` + SVG pequeno que passa o conceito da opção. viewBox `0 0 100 60`, 64px de altura, `stroke="currentColor"`, formas geométricas simples. Exemplos no demo do template (reescrever · documentar · custom). Conceito abstrato demais: pule a ilustração **daquele** card e mantenha nos outros. Parcial é ok; ícone de banco de imagem, não.

## Content rules

- **Titles in bold, NOT backticks** (per CLAUDE.md). Backticks render azul claro — ruim no fundo claro no CLI, mas aqui a gente tá em HTML dark, ok usar `code.inline` para paths/commands.
- **No blockquotes (`>`)** in content rendering — in HTML they're fine but mimic the CLAUDE.md rule of keeping text readable.
- **No markdown tables inside `<details>`** — use the labels pattern (label-row with 🔧💡📁).
- **Svg inline** for diagrams — no external JS libs, no CDN. Nada de rede.
- **Max ~5 top-level `<section>`s** — if you need more, it's too dense for one visual; split into multiple files.
- **Output cru vai literal.** Dentro de `.evidencia`/`<pre class="raw">` não se parafraseia, não se resume e não se "humaniza" — escape só o que o HTML exige (`&lt;`, `&amp;`). Cortar linhas do meio é permitido se marcar com `…`; reescrever, não.

## Hook integration

O hook `${CLAUDE_PLUGIN_ROOT}/hooks/pre-exitplan-visualize.sh` roda no `PreToolUse` de `ExitPlanMode` (registrado em `hooks/hooks.json`). Trabalho dele: **bloquear a apresentação do plano no CLI até o HTML existir E carregar prova**, pra o Pedro ler no browser antes de aprovar.

Flow when the hook blocks (exit 2):

1. You (Claude) just called `ExitPlanMode` with a plan file.
2. `pre-exitplan-visualize.sh` runs → resolves the project's visual dir (cascade) and looks for a recent HTML matching this session in it.
3. Sem HTML recente **ou com HTML sem prova** → exits 2 with stderr instructions. The tool call is BLOCKED. The plan is NOT shown to Pedro.
5. You receive the stderr message and MUST:
   - Invoke this skill (Skill tool with `name: visual`)
   - Read the plan file named in the stderr
   - Render it as HTML using the template above
   - Save to the **exact path suggested in stderr** (the hook already resolved the project dir for you — do not change it)
   - Open with `open "<path>"`
6. Retry `ExitPlanMode`. O hook acha o HTML fresco (< 5 min) **e com prova dentro** → exit 0, plan proceeds to Pedro in the CLI.
7. Pedro reads the HTML in the browser, approves or rejects in the CLI.

Critical behavior when the hook blocks:

- Do NOT try to present the plan as text in your response.
- Do NOT skip the skill — use the template, do not invent your own HTML.
- Do NOT summarize the plan to Pedro — the HTML IS the summary.
- Do the minimum: render, open, retry `ExitPlanMode`. One tight loop.

## Workflow when invoked

1. Identify source content (last message, plan file, explicit content)
2. Detect type (plan / diagnostic / question with options / generic)
3. Resolve the target dir: `DIR=$(bash ${CLAUDE_PLUGIN_ROOT}/skills/visual/resolve-dir.sh "$PWD")`
4. Pick slug from main topic → filename `YYYY-MM-DD-<slug>.html`
5. Write file to `$DIR/<filename>` using template
6. Run `open "$DIR/<filename>"`
7. Tell Pedro in 1-2 lines: "Abri no browser: `<path>`"

Never render and then text-dump the same content in the CLI response. The whole point is: HTML replaces the textão, doesn't duplicate it.
