{"version":3,"file":"workflowProgressOverlay.cjs","names":["truncateToWidth"],"sources":["../../../../src/tui/workflow/workflowProgressOverlay.ts"],"sourcesContent":["/**\n * Task-like live workflow list shown above Pi's editor.\n *\n * DESIGN PATTERNS:\n * - The extension owns run/event state; this component only projects and renders it.\n * - Register the widget once, then request redraws as progress, elapsed time, or the spinner changes.\n * - Terminal runs are absent from `rows`, so the widget disappears when the last run ends.\n *\n * CODING STANDARDS:\n * - Every rendered line is ANSI-aware and truncated to the available terminal width.\n * - Theme colors come from Pi's live UI context so theme changes repaint correctly.\n * - Named exports only and explicit return types on exported members.\n *\n * AVOID:\n * - Persisting progress in the chat transcript.\n * - Starting timers while no workflow is visible.\n */\n\nimport type { WorkflowProgressEvent, WorkflowRunRecord } from '@agimon-ai/workflow-mcp';\nimport type { ExtensionUIContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { type TUI, truncateToWidth } from '@earendil-works/pi-tui';\n\nexport const WORKFLOW_PROGRESS_WIDGET_KEY = 'workflow-mcp-progress';\n\nconst OVERLAY_HEADING = 'Workflows';\nconst PROGRESS_TICK_MS = 200;\nconst RUNNING_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const;\nconst STATIC_PROGRESS_MARKER = '*';\nconst MILLISECONDS_PER_SECOND = 1000;\nconst SECONDS_PER_MINUTE = 60;\nconst MINUTES_PER_HOUR = 60;\nconst STARTING_JOB_STEP = 'Starting job';\nconst STARTING_WORKFLOW_STEP = 'Starting workflow';\nconst PREPARING_WORKFLOW_STEP = 'Preparing workflow';\nconst FINALIZING_WORKFLOW_STEP = 'Finalizing workflow';\n\ntype WorkflowProgressColor = Parameters<Theme['fg']>[0];\n\nexport type WorkflowProgressState = 'completed' | 'failed' | 'paused' | 'running' | 'skipped' | 'starting';\n\n/** One explicit palette makes every workflow state visually stable across the list. */\nexport const WORKFLOW_PROGRESS_COLORS: Readonly<Record<WorkflowProgressState, WorkflowProgressColor>> = {\n  starting: 'dim',\n  running: 'accent',\n  completed: 'success',\n  paused: 'warning',\n  failed: 'error',\n  skipped: 'muted',\n};\n\nexport interface WorkflowProgressRow {\n  job: string;\n  key: string;\n  name: string;\n  startedAt: string;\n  state: WorkflowProgressState;\n  step: string;\n}\n\nfunction latestStep(events: readonly WorkflowProgressEvent[]): WorkflowProgressEvent | undefined {\n  return events.findLast((event) => event.type === 'step' && !!event.step);\n}\n\nfunction latestEvent(events: readonly WorkflowProgressEvent[]): WorkflowProgressEvent | undefined {\n  return events.at(-1);\n}\n\nfunction progressState(record: WorkflowRunRecord, event: WorkflowProgressEvent | undefined): WorkflowProgressState {\n  if (record.stage === 'error') return 'failed';\n  if (record.stage === 'completed') return 'completed';\n  if (record.executionState === 'paused' || record.executionState === 'pause_requested') return 'paused';\n\n  switch (event?.status) {\n    case 'failed':\n      return 'failed';\n    case 'paused':\n    case 'pause_requested':\n      return 'paused';\n    case 'completed':\n      return 'completed';\n    case 'skipped':\n      return 'skipped';\n    case 'resumed':\n    case 'running':\n      return 'running';\n    default:\n      return record.executionCursor?.stepName ? 'running' : 'starting';\n  }\n}\n\nfunction fallbackStep(record: WorkflowRunRecord): string {\n  switch (record.executionCursor?.phase) {\n    case 'pre':\n      return PREPARING_WORKFLOW_STEP;\n    case 'post':\n      return FINALIZING_WORKFLOW_STEP;\n    case 'job':\n    case 'step':\n      return STARTING_JOB_STEP;\n    default:\n      return STARTING_WORKFLOW_STEP;\n  }\n}\n\n/** Derive the single current line for one run from its record and pushed progress events. */\nexport function workflowProgressRow(\n  record: WorkflowRunRecord,\n  events: readonly WorkflowProgressEvent[],\n): WorkflowProgressRow {\n  const stepEvent = latestStep(events);\n  const event = latestEvent(events);\n  const cursor = record.executionCursor;\n  const job = cursor?.job ?? stepEvent?.job ?? event?.job ?? record.job ?? cursor?.phase ?? 'workflow';\n  const step = cursor?.stepName ?? (stepEvent?.job === job ? stepEvent.step : undefined) ?? fallbackStep(record);\n\n  return {\n    job,\n    key: record.runId ?? `${record.workspace}/${record.runKey}`,\n    name: record.displayName,\n    startedAt: record.startedAt,\n    state: progressState(record, event),\n    step,\n  };\n}\n\n/** Task-style elapsed time: whole seconds stay stable and compact between animation ticks. */\nexport function formatWorkflowDuration(durationMs: number): string {\n  if (!Number.isFinite(durationMs) || durationMs < 0) return '?';\n  const totalSeconds = Math.floor(durationMs / MILLISECONDS_PER_SECOND);\n  const seconds = totalSeconds % SECONDS_PER_MINUTE;\n  const totalMinutes = Math.floor(totalSeconds / SECONDS_PER_MINUTE);\n  if (totalMinutes < MINUTES_PER_HOUR) {\n    return totalMinutes === 0 ? `${seconds}s` : `${totalMinutes}m${String(seconds).padStart(2, '0')}s`;\n  }\n  const minutes = totalMinutes % MINUTES_PER_HOUR;\n  const hours = Math.floor(totalMinutes / MINUTES_PER_HOUR);\n  return `${hours}h${String(minutes).padStart(2, '0')}m${String(seconds).padStart(2, '0')}s`;\n}\n\nfunction progressMarker(state: WorkflowProgressState, nowMs: number): string {\n  if (state !== 'running' && state !== 'starting') return STATIC_PROGRESS_MARKER;\n  const tick = Number.isFinite(nowMs) ? Math.floor(Math.max(0, nowMs) / PROGRESS_TICK_MS) : 0;\n  return RUNNING_SPINNER_FRAMES[tick % RUNNING_SPINNER_FRAMES.length] ?? RUNNING_SPINNER_FRAMES[0];\n}\n\n/** Render exactly: `<workflow name>[<duration>][<job>] <activity> <step>`. */\nexport function formatWorkflowProgressRow(row: WorkflowProgressRow, theme: Theme, nowMs = Date.now()): string {\n  const startedAt = Date.parse(row.startedAt);\n  const duration = formatWorkflowDuration(Number.isFinite(startedAt) ? nowMs - startedAt : Number.NaN);\n  const color = WORKFLOW_PROGRESS_COLORS[row.state];\n  return [\n    theme.fg(color, row.name),\n    theme.fg('muted', `[${duration}][${row.job}]`),\n    theme.fg(color, ` ${progressMarker(row.state, nowMs)} `),\n    theme.fg('text', row.step),\n  ].join('');\n}\n\n/** Persistent workflow widget with the same heading/tree shape as Doom's task list. */\nexport class WorkflowProgressOverlay {\n  private rows: readonly WorkflowProgressRow[] = [];\n  private uiCtx: ExtensionUIContext | undefined;\n  private widgetRegistered = false;\n  private tui: TUI | undefined;\n  private progressTimer: NodeJS.Timeout | undefined;\n\n  /** Identity-compare so repeated session starts do not register duplicate widgets. */\n  setUICtx(ctx: ExtensionUIContext): void {\n    if (ctx === this.uiCtx) return;\n    if (this.uiCtx && this.widgetRegistered) this.uiCtx.setWidget(WORKFLOW_PROGRESS_WIDGET_KEY, undefined);\n    this.stopProgressTimer();\n    this.uiCtx = ctx;\n    this.widgetRegistered = false;\n    this.tui = undefined;\n  }\n\n  update(rows: readonly WorkflowProgressRow[]): void {\n    this.rows = rows;\n    if (!this.uiCtx) {\n      this.stopProgressTimer();\n      return;\n    }\n\n    if (rows.length === 0) {\n      if (this.widgetRegistered) {\n        this.uiCtx.setWidget(WORKFLOW_PROGRESS_WIDGET_KEY, undefined);\n        this.widgetRegistered = false;\n        this.tui = undefined;\n      }\n      this.stopProgressTimer();\n      return;\n    }\n\n    if (this.widgetRegistered) {\n      this.syncProgressTimer();\n      this.tui?.requestRender();\n      return;\n    }\n\n    this.uiCtx.setWidget(\n      WORKFLOW_PROGRESS_WIDGET_KEY,\n      (tui, factoryTheme) => {\n        this.tui = tui;\n        return {\n          render: (width: number) => this.renderWidget(this.uiCtx?.theme ?? factoryTheme, width),\n          invalidate: () => {\n            // Nothing cached: the next render reads the current rows, clock, and theme.\n          },\n        };\n      },\n      { placement: 'aboveEditor' },\n    );\n    this.widgetRegistered = true;\n    this.syncProgressTimer();\n  }\n\n  private syncProgressTimer(): void {\n    if (!this.widgetRegistered || this.rows.length === 0) {\n      this.stopProgressTimer();\n      return;\n    }\n    if (this.progressTimer) return;\n    this.progressTimer = setInterval(() => this.tui?.requestRender(), PROGRESS_TICK_MS);\n    this.progressTimer.unref?.();\n  }\n\n  private stopProgressTimer(): void {\n    if (this.progressTimer) clearInterval(this.progressTimer);\n    this.progressTimer = undefined;\n  }\n\n  private renderWidget(theme: Theme, width: number): string[] {\n    if (this.rows.length === 0) return [];\n    const truncate = (line: string): string => truncateToWidth(line, width, '…');\n    const heading = truncate(\n      `${theme.fg('accent', '●')} ${theme.fg('accent', `${OVERLAY_HEADING} (${this.rows.length})`)}`,\n    );\n    const nowMs = Date.now();\n    const lines = [heading];\n\n    for (const [index, row] of this.rows.entries()) {\n      const branch = index === this.rows.length - 1 ? '└─' : '├─';\n      lines.push(truncate(`${theme.fg('dim', branch)} ${formatWorkflowProgressRow(row, theme, nowMs)}`));\n    }\n\n    // Pi adds space above widgets but not below; keep the list off the editor border.\n    lines.push('');\n    return lines;\n  }\n\n  dispose(): void {\n    this.stopProgressTimer();\n    if (this.uiCtx) this.uiCtx.setWidget(WORKFLOW_PROGRESS_WIDGET_KEY, undefined);\n    this.rows = [];\n    this.widgetRegistered = false;\n    this.tui = undefined;\n    this.uiCtx = undefined;\n  }\n}\n"],"mappings":"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"}