Skip to content

harness: Escape should cancel the in-progress turn when the canvas is focused #703

Description

@btipling

Summary

When the harness canvas is focused and a turn is Busy, Escape should cancel that turn — the same abort as the ■ Stop icon (protocol v9 queueCancelFromUi → host takePendingCancelAbortController).

Today Stop is pointer-only. The keyboard table has Enter / Ctrl+Enter / arrows. Escape does nothing to the in-flight round, so a keyboard operator has no cancel chord.

Observed

  1. Focus the canvas (#harness-canvas, tabIndex={0} when ready).
  2. Send a prompt so lifecycle is Busy (Waiting for model / tools).
  3. Press Escape.

Actual: the turn keeps running. Composer may keep the caret. Only ■ Stop (or host nav away) aborts.

Expected: Escape aborts the in-flight round the same way ■ does — pending cancel, host abort, existing Stop/error paint. Composer text is left alone (Stop does not clear the field).

Idle (no turn) Escape stays a no-op for cancel — do not invent “clear composer” or “leave the page.”

Likely seam

Cancel path already exists; it is not bound to a key.

Piece Role
native/harness/src/ui.zig ■ button (Busy only) bridge.queueCancelFromUi()
native/harness/src/bridge.zig queueCancelFromUi No-op unless lifecycle == .busy or pending submit; sets has_pending_cancel
Host poll HarnessHost.tsx takePendingCancel()abortRef.current?.abort()
Composer event scan (ui.zig) Ctrl/Cmd+Enter + ↑/↓ history — no .escape
Keyboard table docs/harness-limits.md — no Escape row

Do not add a second cancel protocol. Bind Escape to the existing v9 Stop path.

Who already owns Escape

Context Current Esc Keep
DOM NavMenu open closes the menu (app/components/NavMenu.tsx) yes — host chrome, not canvas
Queue-row editor open queue_band.handleEscapecancelEdit() (does not abort the turn) yes — closer overlay wins
Model picker no Esc handler today if Esc should close the picker first, lock that; otherwise Busy cancel is fine
Busy, harness focused, no overlay nothing this issue

Scan Escape at frame scope (canvas focused), not only when the composer TextEntry is focused — “harness focused” includes transcript click / canvas tabIndex. Queue-edit handler already runs during queue_band.paint; it marks the event handled, so a later Busy cancel scan must skip handled events (same pattern as the composer Enter chord).

Goal

  • Escape while Busy + harness focused → same cancel as ■ Stop.
  • Idle → no cancel, no composer wipe.
  • Queue-row edit still dismisses the editor first (no turn abort on that stroke).
  • Open DOM nav menu still closes on Esc (focus is not the canvas).
  • Docs: one Keyboard row in docs/harness-limits.md.

Non-goals

  • New bridge exports / protocol bump (v9 cancel is enough).
  • Esc to close the tab / blur the canvas / Clear session.
  • Dual DOM Stop button.
  • Changing abort semantics (partial transcript persist, cwd-on-cancel, usage carry-forward stay as they are).

Related

  • native/harness/src/ui.zig — Busy ■ → queueCancelFromUi; composer key scan
  • native/harness/src/bridge.zigqueueCancelFromUi / inv_has_pending_cancel
  • native/harness/src/ui/queue_band.zig — Esc dismisses queue-row edit
  • app/harness/HarnessHost.tsx — canvas tabIndex={0}; poll takePendingCancel
  • docs/harness-limits.md — Keyboard & focus

Suggested next: start a turn, keep the canvas focused, press Esc — the busy row should end like ■ Stop. Then Esc while editing a queued follow-up should only close the editor.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingharnessHarness / agent UIui

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions