{"version":3,"file":"workflowOverlay.cjs","names":["isKeyRelease","matchesKey"],"sources":["../../../../src/tui/workflow/workflowOverlay.ts"],"sourcesContent":["/**\n * Input plumbing for the run panel: keystroke batching and the escape hatch.\n *\n * The panel's rendering lives in `workflowRunPanel.ts`, which is a doom overlay;\n * what stays here is the behaviour that has nothing to do with drawing.\n *\n * DESIGN PATTERNS:\n * - Input batching. Every keystroke sent individually costs one subprocess\n *   round trip, measured at ~18ms, which caps sustained typing near 56 c/s and\n *   makes held keys queue visibly. Coalescing a frame's bytes into one send\n *   keeps latency at roughly one frame no matter how fast the user types.\n * - Stateless render. The component recomputes from the latest snapshot on\n *   every frame, so a theme change needs no cache invalidation.\n * - Terminal-independent escape hatch. The focused panel forwards every key to\n *   the run, so the way out cannot itself depend on a modifier the terminal\n *   may not transmit.\n *\n * CODING STANDARDS:\n * - Named exports only\n * - Explicit return types on exported members\n *\n * AVOID:\n * - Sending per keystroke: it is the difference between usable and unusable\n * - Holding the flush timer open after dispose, which would keep writing into\n *   a run the user has stopped watching\n */\n\nimport { isKeyRelease, matchesKey } from '@earendil-works/pi-tui';\n\n/** One frame at 60Hz. Long enough to coalesce a burst, short enough to feel direct. */\nconst FLUSH_INTERVAL_MS = 16;\n\n/**\n * How close two Escapes must be to read as \"get me out\".\n *\n * Long enough to be reachable without hurrying, short enough that two\n * deliberate, separate interrupts sent to the run are not mistaken for it.\n */\nexport const DOUBLE_ESCAPE_WINDOW_MS = 350;\n\n/** What the overlay should do with a keystroke. */\nexport type EscapeAction = 'forward' | 'close';\n\n/**\n * The panel's guaranteed way out: press Escape twice, quickly.\n *\n * A focused panel hands every key to the run so its agent stays interruptible,\n * which leaves the two `ctrl+alt` chords as the only exits. Those are exactly\n * the keys a terminal may not deliver: macOS without Option-as-Meta sends plain\n * `ctrl+w` instead, and the byte is forwarded into the run rather than matching.\n * The user is then inside a view they cannot leave. Escape is the one key every\n * terminal transmits identically, which is what makes it the failsafe.\n *\n * The first Escape is still forwarded, so interrupting the step stays as fast as\n * it ever was. Only the second one is consumed, and only when it lands inside\n * the window. The cost is one keystroke: sending the run two literal Escapes in\n * quick succession is no longer possible, which is worth an escapable panel.\n */\nexport class DoubleEscapeDetector {\n  private lastEscapeAt: number | undefined;\n\n  constructor(private readonly windowMs: number = DOUBLE_ESCAPE_WINDOW_MS) {}\n\n  /**\n   * Classify one chunk of terminal input.\n   *\n   * `now` is injected so the decision is a pure function of its inputs, which\n   * is what makes the window testable without timers.\n   */\n  observe(data: string, now: number = Date.now()): EscapeAction {\n    // Two Escapes coalesced into one read. A terminal in legacy mode delivers\n    // this when they arrive inside the same poll, so it is the same gesture and\n    // has to reach the same answer. `matchesKey` never sees it as an escape:\n    // it parses as ctrl+alt+[, which has no meaning to a run either way.\n    if (data === '\\x1b\\x1b') {\n      this.lastEscapeAt = undefined;\n      return 'close';\n    }\n\n    // A Kitty release event repeats the press's own bytes. Counting it would\n    // turn one deliberate press into a pair and close the panel under the user.\n    if (isKeyRelease(data)) return 'forward';\n\n    if (!matchesKey(data, 'escape')) {\n      // Anything typed in between makes these two separate interrupts rather\n      // than one gesture, so the pair has to start over.\n      this.lastEscapeAt = undefined;\n      return 'forward';\n    }\n\n    if (this.lastEscapeAt !== undefined && now - this.lastEscapeAt <= this.windowMs) {\n      this.lastEscapeAt = undefined;\n      return 'close';\n    }\n\n    this.lastEscapeAt = now;\n    return 'forward';\n  }\n}\n\n/**\n * Collects keystrokes and forwards them in frame-sized batches.\n *\n * Failures are swallowed on purpose: a dropped keystroke is a nuisance, but an\n * exception raised from a terminal input handler would tear down the overlay.\n */\nexport class TerminalInputBatcher {\n  private pending = '';\n  private timer: NodeJS.Timeout | undefined;\n  private disposed = false;\n\n  constructor(\n    private readonly send: (text: string) => Promise<void>,\n    private readonly flushIntervalMs: number = FLUSH_INTERVAL_MS,\n  ) {}\n\n  write(data: string): void {\n    if (this.disposed || data.length === 0) return;\n    this.pending += data;\n    if (this.timer) return;\n    this.timer = setTimeout(() => void this.flush(), this.flushIntervalMs);\n    this.timer.unref?.();\n  }\n\n  /** Send whatever has accumulated. Safe to call with nothing pending. */\n  async flush(): Promise<void> {\n    if (this.timer) clearTimeout(this.timer);\n    this.timer = undefined;\n    const text = this.pending;\n    this.pending = '';\n    if (!text || this.disposed) return;\n    try {\n      await this.send(text);\n    } catch {\n      // A dropped keystroke must not take the overlay down with it.\n    }\n  }\n\n  /** Stop accepting and forwarding input. Pending bytes are discarded. */\n  dispose(): void {\n    this.disposed = true;\n    if (this.timer) clearTimeout(this.timer);\n    this.timer = undefined;\n    this.pending = '';\n  }\n\n  /** Exposed for tests: whether a flush is scheduled. */\n  hasPending(): boolean {\n    return this.pending.length > 0;\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,oBAAoB;;;;;;;;;;;;;;;;AA4B1B,IAAa,uBAAb,MAAkC;CAGH;CAF7B;CAEA,YAAY,WAAiB,KAA4C;EAA5C,KAAA,WAAA;CAA6C;;;;;;;CAQ1E,QAAQ,MAAc,MAAc,KAAK,IAAI,GAAiB;EAK5D,IAAI,SAAS,YAAY;GACvB,KAAK,eAAe,KAAA;GACpB,OAAO;EACT;EAIA,KAAA,GAAIA,uBAAAA,aAAAA,CAAa,IAAI,GAAG,OAAO;EAE/B,IAAI,EAAA,GAACC,uBAAAA,WAAAA,CAAW,MAAM,QAAQ,GAAG;GAG/B,KAAK,eAAe,KAAA;GACpB,OAAO;EACT;EAEA,IAAI,KAAK,iBAAiB,KAAA,KAAa,MAAM,KAAK,gBAAgB,KAAK,UAAU;GAC/E,KAAK,eAAe,KAAA;GACpB,OAAO;EACT;EAEA,KAAK,eAAe;EACpB,OAAO;CACT;AACF;;;;;;;AAQA,IAAa,uBAAb,MAAkC;CAMb;CACA;CANnB,UAAkB;CAClB;CACA,WAAmB;CAEnB,YACE,MACA,kBAA2C,mBAC3C;EAFiB,KAAA,OAAA;EACA,KAAA,kBAAA;CAChB;CAEH,MAAM,MAAoB;EACxB,IAAI,KAAK,YAAY,KAAK,WAAW,GAAG;EACxC,KAAK,WAAW;EAChB,IAAI,KAAK,OAAO;EAChB,KAAK,QAAQ,iBAAiB,KAAK,KAAK,MAAM,GAAG,KAAK,eAAe;EACrE,KAAK,MAAM,QAAQ;CACrB;;CAGA,MAAM,QAAuB;EAC3B,IAAI,KAAK,OAAO,aAAa,KAAK,KAAK;EACvC,KAAK,QAAQ,KAAA;EACb,MAAM,OAAO,KAAK;EAClB,KAAK,UAAU;EACf,IAAI,CAAC,QAAQ,KAAK,UAAU;EAC5B,IAAI;GACF,MAAM,KAAK,KAAK,IAAI;EACtB,QAAQ,CAER;CACF;;CAGA,UAAgB;EACd,KAAK,WAAW;EAChB,IAAI,KAAK,OAAO,aAAa,KAAK,KAAK;EACvC,KAAK,QAAQ,KAAA;EACb,KAAK,UAAU;CACjB;;CAGA,aAAsB;EACpB,OAAO,KAAK,QAAQ,SAAS;CAC/B;AACF"}