dsh-goal-pause-guard
Plugin para o DeepSeek Harness: modal de confirmação antes de pausar uma atividade rodando.
Pausar uma goal não é um gesto pequeno: ela interrompe a rodada atual e todas as subtarefas em execução na hora. O plugin pede confirmação antes.

Porquê
No harness, pausar uma goal armada dispara uma cascata (motorizada no
goal-round-driver, goal/changed → agent.cancel, keepInbox):
- a rodada em andamento é abortada — o agente para de executar no meio do que estava fazendo;
- as subtarefas em execução param junto (tools, comandos, subagentes);
- a continuação automática fica desarmada — nada de novas rodadas até um
resumehumano.
É exatamente esse efeito que o plugin coloca atrás de um portão: um clique acidental no botão de pause não pode desligar uma atividade que está rodando.
O que o plugin faz
clique em "Pause goal" (clique humano, trusted)
│
▼
┌──────────────┐ captura no document (capture phase, antes do React)
│ GUARD │ isPauseButton? ── não ──► o clique passa (fail-open)
└──────┬───────┘
▼ sim
MODAL DE CONFIRMAÇÃO (pt-BR / en / zh, tema claro-escuro do host)
├── Cancelar / Esc / fundo ──► nada acontece
└── Confirmar ──► re-clique UNTRUSTED no botão de pause ATUAL
│ (passa pelo guard sem re-gate)
▼
GoalBar → goals.pause (Remote RPC)
▼
host: goal paused + rodada abortada + subtasks param
- Detecção por dois canais — o botão é reconhecido pelo
aria-labellocalizado (Pause goal/暂停目标, os dois dicionários da barra de goal) ou pelo path SVG do ícone de pause (IconPauseOutline16), sempre dentro de[data-goal-bar](o marcador que o próprio e2e oficial usa). - Fail-open por projeto — se o botão não for identificável, o clique passa e o pause se comporta exatamente como sem o plugin. Um portão de confirmação nunca pode quebrar a ação que protege.
- Sem loop, sem referência velha — o confirm re-localiza o botão de pause atual antes de re-clicar (um re-render entre abrir e confirmar nunca transforma "confirmar" em "clicar outra coisa"); botão desabilitado ou removido nesse meio-tempo = nada acontece.
- Um modal por vez — Esc, fundo e Cancelar fecham; o foco vai ao botão de confirmar e volta ao botão de pause no fechar.
- Determinismo — sem timers, sem rede, sem storage. A metade host é um
applyvazio (mesmo padrão do@deepseek-ai/dsh-client-ui-goal): nenhum estado do harness é tocado.
Instalação
Como bundle do profile web — o dsh.bundle.patch do pacote ativa a camada
sozinho (insert próprio, sem colidir com nenhum outro id):
dsh plugin --profile web add /caminho/para/dsh-goal-pause-guard
dsh web # reinicie a instância; plugins entram no boot
Desinstalar: remova a linha do bundle do ~/.dsh/profiles/web/package.json
(dsh plugin --profile web remove dsh-goal-pause-guard) e reinicie.
Verificação
Toda a verificação roda isolada — nenhum teste toca uma instância real do
DSH (nem a porta 3080, nem o $DSH_HOME):
pnpm install
pnpm run typecheck # tsc estrito
pnpm run build # dist/ (host) + lib/client.js (browser, closure-factory)
pnpm test # 23 testes: detector + comportamento + e2e do artefato
| Camada | Prova |
|---|---|
| Detector (unit, jsdom) | os dois canais de identificação; rejeição de todo o resto (resume/edit/clear, botões fora da barra, ícones estrangeiros, prefixos parecidos) |
| Comportamento (jsdom) | clique humano abre o modal e nunca chega ao app; confirmar re-clica UMA vez como untrusted; cancelar/Esc/fundo não pausam; segundo clique engolido; botão removido/desabilitado no confirm não dispara; disposer remove tudo |
| E2E do artefato | o lib/client.js enviado é carregado como o harness carrega (window.__ModuleLoader__.load({id, factory})) e o fluxo inteiro é dirigido sobre ele |
O screenshot acima é real: capturado num boot isolado ( $DSH_HOME
temporária + porta própria) com Chromium headless, clicando no botão de pause
de verdade (evento trusted via CDP) — o guard abriu o modal e o cancelamento
não enviou pausa nenhuma.
Estrutura
dsh-goal-pause-guard/
├── package.json # dsh.bundle.patch + dsh.client (platform web) + exports["./client"]
├── cordis.patch.yml # camada 1 (bundle): insert próprio, sem !!js, sem config
├── src/index.ts # host half: apply vazio (entrada do Loader)
├── client/
│ ├── guard-core.ts # TODA a lógica: detector, textos, modal, install (0 imports)
│ ├── index.ts # plugin body: inject [] + apply(ctx) via ctx.effect
│ ├── guard.css # modal com tokens --dsw-* (claro/escuro), classes gpg-
│ └── client.d.ts # declaração do subpath ./client
├── scripts/build-client.mjs # esbuild → lib/client.js (closure-factory, CSS embutido)
├── test/ # jsdom + node:test, offline por projeto
└── dist/, lib/ # produtos do build (host half / browser bundle)
Compatibilidade
Verificado contra DSH 0.1.5-alpha.2 (a linha viva instalada, bootada do
source). O detector ancora em dois fatos estáveis da superfície — data-goal-bar
e o aria-label do action.pause do dicionário goal. Se o harness mudar os
dois ao mesmo tempo, o guard fica fail-open (pause sem modal) — nunca quebrado.
Licença
MIT © Frederico Kluser
No comments yet. Be the first to write one.