Skip to content

feat: ESC não interrompe a geração em andamento — portar solução do upstream anomalyco/opencode #1024

@ElioNeto

Description

@ElioNeto

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

  • Garantir que DEFAULT_KEYBINDS.interrupt contém [{ key: "escape" }] (ou equivalente renomeado para teamcode)
  • Verificar que promptHit(keys().interrupts, key) está conectado corretamente no onKeyDown do footer.prompt.tsx (ou equivalente)
  • Implementar/corrigir handleInterrupt no RunFooter (ou equivalente) com o padrão two-press e o estado interrupt no FooterState
  • Garantir que onInterrupt no runtime chama o método de abort da sessão ativa (state.aborting = true + chamada de abort na API)
  • Verificar que Effect.onInterrupt(() => controller.abort()) está presente no pipeline de prompt/LLM
  • O interruptHint deve ser gerado via printableBinding(options.keybinds.interrupt, ...) para exibir o atalho correto no status

Contexto adicional

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions