{"version":3,"file":"workflowRunPanel.mjs","names":[],"sources":["../../../../src/tui/workflow/workflowRunPanel.ts"],"sourcesContent":["/**\n * Run panel: the live view of one run's job tree and its terminal.\n *\n * A doom overlay rather than the bare header/body/footer it used to draw, so the\n * one surface a user sits inside for minutes at a time carries the same frame,\n * breadcrumb and key legend as every other. The base class owns the chrome and\n * hands the body an exact height, which also retires the hand-rolled row\n * arithmetic that existed only to stop Pi clipping the footer.\n *\n * Key routing is the reason this is a component rather than a renderer: a\n * focused panel receives input before Pi's global dispatcher, so the view\n * controls have to be handled here or the run's TTY swallows them.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { type KeyId, matchesKey } from '@earendil-works/pi-tui';\n\nimport {\n  DOOM_FULLSCREEN_UI_OPTIONS,\n  DOOM_OVERLAY_ACCENT,\n  DoomOverlay,\n  type DoomOverlayChrome,\n  type DoomOverlayTui,\n} from './doomOverlay';\nimport { fit, fitTerminalLine } from './overlayText';\nimport { DoubleEscapeDetector } from './workflowOverlay';\n\nconst TITLE = 'WORKFLOW RUN';\nconst VIEW_ONLY = 'view only · this run is hosted natively';\nconst OUTPUT_HEADING = 'OUTPUT · newest last';\nconst WAITING = 'Waiting for output…';\n/** A divider and its heading earn their rows only when output sits beneath them. */\nconst OUTPUT_CHROME_ROWS = 2;\n\nexport interface WorkflowRunPanelSnapshot {\n  /** Job tree lines, already themed by the caller. */\n  progress: string[];\n  /** Recent terminal output for the running step. */\n  output: string[];\n}\n\nexport interface WorkflowRunPanelOptions {\n  runKey: string;\n  /** Full run label for the header, e.g. `name · workspace/runKey`. */\n  label: string;\n  breadcrumb: string;\n  /** False for a natively hosted run, which has no terminal to type into. */\n  interactive: boolean;\n  /** Plain-sentence footer, used when the terminal cannot fit the key caps. */\n  footer: string;\n  /** Key legend, which differs between a typeable panel and a view. */\n  hints: readonly (readonly [string, string])[];\n  /** Given the body width, because the job tree is fitted as it is built. */\n  snapshot: (width: number) => WorkflowRunPanelSnapshot;\n  /**\n   * The columns and rows the output pane can actually show, reported on each\n   * paint so a launcher that supports resizing can match the run to them.\n   * Called from render, so an implementation must not do the work inline: the\n   * run's geometry changes out of band and the next poll picks up the reflow.\n   */\n  onViewport?: (columns: number, rows: number) => void;\n  /** Hand typing back to the session, leaving the panel open. */\n  onUnfollow: () => void;\n  onClose: () => void;\n  /** Forward a keystroke to the run. */\n  sendInput: (data: string) => void;\n  /**\n   * Send whatever is buffered before the panel goes away, or the Escape that\n   * closed it dies with the batcher.\n   */\n  flushInput: () => void;\n  unfollowShortcut: KeyId;\n  closeShortcut: KeyId;\n  onDispose: () => void;\n}\n\nexport class WorkflowRunPanelComponent extends DoomOverlay {\n  private readonly escapes = new DoubleEscapeDetector();\n\n  constructor(\n    tui: DoomOverlayTui,\n    theme: Theme,\n    private readonly options: WorkflowRunPanelOptions,\n  ) {\n    super(tui, theme);\n  }\n\n  /**\n   * The chords are matched against the whole chunk, as Pi delivers one per key\n   * event. A chord buried in a coalesced burst therefore misses, which is\n   * acceptable now that double-Escape guarantees an exit: scanning for the bytes\n   * anywhere in a chunk would instead eat legitimate input, since `\\x1b\\x17` is\n   * also a valid Escape-then-ctrl+w for the run's own editor.\n   */\n  handleInput(data: string): void {\n    if (matchesKey(data, this.options.unfollowShortcut)) {\n      this.options.onUnfollow();\n      return;\n    }\n    if (matchesKey(data, this.options.closeShortcut)) {\n      this.options.onClose();\n      return;\n    }\n    // The failsafe. The first Escape falls through to the run, so interrupting\n    // the step is as fast as it ever was; only the second one inside the window\n    // is consumed.\n    if (this.escapes.observe(data) === 'close') {\n      this.options.flushInput();\n      this.options.onClose();\n      return;\n    }\n    this.options.sendInput(data);\n  }\n\n  protected getChrome(): DoomOverlayChrome {\n    return {\n      title: TITLE,\n      accent: DOOM_OVERLAY_ACCENT,\n      breadcrumb: this.options.breadcrumb,\n      headerRight: this.options.interactive ? this.options.label : `${this.options.label} · ${VIEW_ONLY}`,\n      footer: this.options.footer,\n      footerHints: this.options.hints,\n      footerRight: this.options.interactive ? 'typing goes to the run' : 'typing stays here',\n    };\n  }\n\n  /**\n   * The tree is never truncated: knowing which job is running matters more than\n   * seeing another line of output, so the terminal pane absorbs whatever the\n   * tree does not use.\n   */\n  protected renderBody(width: number, height: number): string[] {\n    const { progress, output } = this.options.snapshot(width);\n    const tree = progress.slice(0, height).map((line) => fit(line, width));\n\n    const spare = height - tree.length;\n    if (spare <= OUTPUT_CHROME_ROWS) return tree.slice(0, height);\n\n    const rows = spare - OUTPUT_CHROME_ROWS;\n    this.options.onViewport?.(width, rows);\n    // Output keeps its own colour, so it is fitted by the escape-preserving\n    // path rather than the themed-row one; see `fitTerminalLine`.\n    const tail = output.slice(-rows).map((line) => fitTerminalLine(line, width));\n    return [\n      ...tree,\n      '',\n      this.theme.fg('dim', OUTPUT_HEADING),\n      ...(tail.length > 0 ? tail : [this.theme.fg('dim', WAITING)]),\n    ].slice(0, height);\n  }\n\n  dispose(): void {\n    this.options.onDispose();\n  }\n}\n\n/**\n * A panel whose keys cannot reach anything must never take them. Pi focuses an\n * overlay on show unless told otherwise, which for a native run would swallow\n * the user's typing into a run with no terminal to receive it.\n */\nexport function runPanelUiOptions(interactive: boolean): Record<string, unknown> {\n  return {\n    ...DOOM_FULLSCREEN_UI_OPTIONS,\n    overlayOptions: {\n      ...DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions,\n      ...(interactive ? {} : { nonCapturing: true }),\n    },\n  };\n}\n\nexport type WorkflowRunPanelHost = Pick<ExtensionContext['ui'], 'custom'>;\n"],"mappings":";;;;;AA2BA,MAAM,QAAQ;AACd,MAAM,YAAY;AAClB,MAAM,iBAAiB;AACvB,MAAM,UAAU;;AAEhB,MAAM,qBAAqB;AA4C3B,IAAa,4BAAb,cAA+C,YAAY;CAMtC;CALnB,UAA2B,IAAI,qBAAqB;CAEpD,YACE,KACA,OACA,SACA;EACA,MAAM,KAAK,KAAK;EAFC,KAAA,UAAA;CAGnB;;;;;;;;CASA,YAAY,MAAoB;EAC9B,IAAI,WAAW,MAAM,KAAK,QAAQ,gBAAgB,GAAG;GACnD,KAAK,QAAQ,WAAW;GACxB;EACF;EACA,IAAI,WAAW,MAAM,KAAK,QAAQ,aAAa,GAAG;GAChD,KAAK,QAAQ,QAAQ;GACrB;EACF;EAIA,IAAI,KAAK,QAAQ,QAAQ,IAAI,MAAM,SAAS;GAC1C,KAAK,QAAQ,WAAW;GACxB,KAAK,QAAQ,QAAQ;GACrB;EACF;EACA,KAAK,QAAQ,UAAU,IAAI;CAC7B;CAEA,YAAyC;EACvC,OAAO;GACL,OAAO;GACP,QAAQ;GACR,YAAY,KAAK,QAAQ;GACzB,aAAa,KAAK,QAAQ,cAAc,KAAK,QAAQ,QAAQ,GAAG,KAAK,QAAQ,MAAM,KAAK;GACxF,QAAQ,KAAK,QAAQ;GACrB,aAAa,KAAK,QAAQ;GAC1B,aAAa,KAAK,QAAQ,cAAc,2BAA2B;EACrE;CACF;;;;;;CAOA,WAAqB,OAAe,QAA0B;EAC5D,MAAM,EAAE,UAAU,WAAW,KAAK,QAAQ,SAAS,KAAK;EACxD,MAAM,OAAO,SAAS,MAAM,GAAG,MAAM,CAAC,CAAC,KAAK,SAAS,IAAI,MAAM,KAAK,CAAC;EAErE,MAAM,QAAQ,SAAS,KAAK;EAC5B,IAAI,SAAS,oBAAoB,OAAO,KAAK,MAAM,GAAG,MAAM;EAE5D,MAAM,OAAO,QAAQ;EACrB,KAAK,QAAQ,aAAa,OAAO,IAAI;EAGrC,MAAM,OAAO,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,SAAS,gBAAgB,MAAM,KAAK,CAAC;EAC3E,OAAO;GACL,GAAG;GACH;GACA,KAAK,MAAM,GAAG,OAAO,cAAc;GACnC,GAAI,KAAK,SAAS,IAAI,OAAO,CAAC,KAAK,MAAM,GAAG,OAAO,OAAO,CAAC;EAC7D,CAAC,CAAC,MAAM,GAAG,MAAM;CACnB;CAEA,UAAgB;EACd,KAAK,QAAQ,UAAU;CACzB;AACF;;;;;;AAOA,SAAgB,kBAAkB,aAA+C;CAC/E,OAAO;EACL,GAAG;EACH,gBAAgB;GACd,GAAG,2BAA2B;GAC9B,GAAI,cAAc,CAAC,IAAI,EAAE,cAAc,KAAK;EAC9C;CACF;AACF"}