Descrição do Problema
O atalho ESC (ou a tecla configurada em session.interrupt) não está interrompendo a geração do assistente quando pressionado durante uma resposta em andamento. Diversas tentativas de implementação foram feitas, mas o comportamento de abort não é acionado corretamente.
Como o upstream resolveu
O repositório upstream anomalyco/opencode implementa uma solução robusta e bem estruturada para isso. A lógica está distribuída em três camadas:
1. Keybind padrão — runtime.boot.ts
O keybind padrão para interrupção é mapeado para escape no objeto DEFAULT_KEYBINDS:
// packages/opencode/src/cli/cmd/run/runtime.boot.ts
const DEFAULT_KEYBINDS: FooterKeybinds = {
// ...
interrupt: [{ key: "escape" }],
// ...
}
Quando há configuração do usuário, o keybind é lido via config.keybinds.get("session.interrupt").
2. Detecção do keypress — footer.prompt.tsx
No handler onKeyDown do prompt, a tecla pressionada é comparada com keys().interrupts usando promptHit. Se houver match e onInterrupt() retornar true, o evento é prevenido:
// packages/opencode/src/cli/cmd/run/footer.prompt.tsx
if (promptHit(keys().interrupts, key)) {
if (input.onInterrupt()) {
event.preventDefault()
return
}
}
3. Lógica de dois pressionamentos — footer.ts (handleInterrupt)
O RunFooter implementa um padrão two-press para evitar interrupções acidentais:
// packages/opencode/src/cli/cmd/run/footer.ts
private handleInterrupt = (): boolean => {
if (this.isClosed || this.state().phase !== "running") {
return false // Só age se há geração em andamento
}
const next = this.state().interrupt + 1
this.patch({ interrupt: next })
if (next < 2) {
this.armInterruptTimer() // Timer de 5s para reset
this.patch({ status: `${this.interruptHint} again to interrupt` })
return true // Primeiro ESC: mostra hint
}
this.clearInterruptTimer()
this.patch({ interrupt: 0, status: "interrupting" })
this.options.onInterrupt?.() // Segundo ESC: chama o abort real
return true
}
4. Abort no backend — processor.ts e prompt.ts
O onInterrupt na camada de sessão usa Effect.onInterrupt + controller.abort() para sinalizar o AbortController da requisição LLM:
// packages/opencode/src/session/prompt.ts
.pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort())))
O que precisa ser implementado no teamcode
Contexto adicional
- Upstream de referência: https://github.com/anomalyco/opencode
- Arquivos-chave no upstream:
- O comportamento esperado é: primeiro ESC exibe
"esc again to interrupt" na status bar; segundo ESC dentro de 5 segundos efetivamente cancela a geração.
- Quando a fase (
state.phase) não for "running", o handler deve retornar false sem fazer nada.
Descrição do Problema
O atalho ESC (ou a tecla configurada em
session.interrupt) não está interrompendo a geração do assistente quando pressionado durante uma resposta em andamento. Diversas tentativas de implementação foram feitas, mas o comportamento de abort não é acionado corretamente.Como o upstream resolveu
O repositório upstream anomalyco/opencode implementa uma solução robusta e bem estruturada para isso. A lógica está distribuída em três camadas:
1. Keybind padrão —
runtime.boot.tsO keybind padrão para interrupção é mapeado para
escapeno objetoDEFAULT_KEYBINDS:Quando há configuração do usuário, o keybind é lido via
config.keybinds.get("session.interrupt").2. Detecção do keypress —
footer.prompt.tsxNo handler
onKeyDowndo prompt, a tecla pressionada é comparada comkeys().interruptsusandopromptHit. Se houver match eonInterrupt()retornartrue, o evento é prevenido:3. Lógica de dois pressionamentos —
footer.ts(handleInterrupt)O
RunFooterimplementa um padrão two-press para evitar interrupções acidentais:4. Abort no backend —
processor.tseprompt.tsO
onInterruptna camada de sessão usaEffect.onInterrupt+controller.abort()para sinalizar oAbortControllerda requisição LLM:O que precisa ser implementado no teamcode
DEFAULT_KEYBINDS.interruptcontém[{ key: "escape" }](ou equivalente renomeado para teamcode)promptHit(keys().interrupts, key)está conectado corretamente noonKeyDowndofooter.prompt.tsx(ou equivalente)handleInterruptnoRunFooter(ou equivalente) com o padrão two-press e o estadointerruptnoFooterStateonInterruptno runtime chama o método de abort da sessão ativa (state.aborting = true+ chamada de abort na API)Effect.onInterrupt(() => controller.abort())está presente no pipeline de prompt/LLMinterruptHintdeve ser gerado viaprintableBinding(options.keybinds.interrupt, ...)para exibir o atalho correto no statusContexto adicional
packages/opencode/src/cli/cmd/run/runtime.boot.ts— DEFAULT_KEYBINDSpackages/opencode/src/cli/cmd/run/footer.prompt.tsx— onKeyDown + promptHit(interrupts)packages/opencode/src/cli/cmd/run/footer.ts— handleInterrupt (two-press pattern)packages/opencode/src/session/prompt.ts— Effect.onInterrupt + controller.abort()"esc again to interrupt"na status bar; segundo ESC dentro de 5 segundos efetivamente cancela a geração.state.phase) não for"running", o handler deve retornarfalsesem fazer nada.