@agimon-ai/doompi-workflow 0.0.1-alpha.2 → 0.0.1-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/README.md +47 -21
  2. package/dist/adapters/pi/dispatcher.cjs.map +1 -0
  3. package/dist/{dispatcher.d.cts → adapters/pi/dispatcher.d.cts} +1 -1
  4. package/dist/adapters/pi/dispatcher.d.cts.map +1 -0
  5. package/dist/{dispatcher.d.mts → adapters/pi/dispatcher.d.mts} +1 -1
  6. package/dist/adapters/pi/dispatcher.d.mts.map +1 -0
  7. package/dist/adapters/pi/dispatcher.mjs.map +1 -0
  8. package/dist/adapters/pi/extension.cjs +2 -0
  9. package/dist/adapters/pi/extension.cjs.map +1 -0
  10. package/dist/{extension.d.cts → adapters/pi/extension.d.cts} +1 -1
  11. package/dist/adapters/pi/extension.d.cts.map +1 -0
  12. package/dist/{extension.d.mts → adapters/pi/extension.d.mts} +1 -1
  13. package/dist/adapters/pi/extension.d.mts.map +1 -0
  14. package/dist/adapters/pi/extension.mjs +2 -0
  15. package/dist/adapters/pi/extension.mjs.map +1 -0
  16. package/dist/adapters/pi/leader.cjs +2 -0
  17. package/dist/adapters/pi/leader.cjs.map +1 -0
  18. package/dist/adapters/pi/leader.mjs +2 -0
  19. package/dist/adapters/pi/leader.mjs.map +1 -0
  20. package/dist/adapters/pi/workflow/piExtension.cjs +11 -0
  21. package/dist/adapters/pi/workflow/piExtension.cjs.map +1 -0
  22. package/dist/adapters/pi/workflow/piExtension.mjs +11 -0
  23. package/dist/adapters/pi/workflow/piExtension.mjs.map +1 -0
  24. package/dist/{workflow → adapters/pi/workflow}/piToolBridge.cjs +1 -1
  25. package/dist/adapters/pi/workflow/piToolBridge.cjs.map +1 -0
  26. package/dist/{workflow → adapters/pi/workflow}/piToolBridge.mjs +1 -1
  27. package/dist/adapters/pi/workflow/piToolBridge.mjs.map +1 -0
  28. package/dist/adapters/pi/workflow/piTools.cjs +5 -0
  29. package/dist/adapters/pi/workflow/piTools.cjs.map +1 -0
  30. package/dist/adapters/pi/workflow/piTools.mjs +5 -0
  31. package/dist/adapters/pi/workflow/piTools.mjs.map +1 -0
  32. package/dist/adapters/pi/workflow/workflowLauncher.cjs +2 -0
  33. package/dist/adapters/pi/workflow/workflowLauncher.cjs.map +1 -0
  34. package/dist/adapters/pi/workflow/workflowLauncher.mjs +2 -0
  35. package/dist/adapters/pi/workflow/workflowLauncher.mjs.map +1 -0
  36. package/dist/extensions/dispatcher.cjs +1 -1
  37. package/dist/extensions/dispatcher.cjs.map +1 -1
  38. package/dist/extensions/dispatcher.d.cts +1 -1
  39. package/dist/extensions/dispatcher.d.mts +1 -1
  40. package/dist/extensions/dispatcher.mjs +1 -1
  41. package/dist/extensions/dispatcher.mjs.map +1 -1
  42. package/dist/extensions/pi.cjs +1 -1
  43. package/dist/extensions/pi.cjs.map +1 -1
  44. package/dist/extensions/pi.d.cts +1 -1
  45. package/dist/extensions/pi.d.mts +1 -1
  46. package/dist/extensions/pi.mjs +1 -1
  47. package/dist/extensions/pi.mjs.map +1 -1
  48. package/dist/index.cjs +1 -1
  49. package/dist/index.d.cts +2 -2
  50. package/dist/index.d.mts +2 -2
  51. package/dist/index.mjs +1 -1
  52. package/dist/schemas/workflowPi.cjs +2 -0
  53. package/dist/schemas/workflowPi.cjs.map +1 -0
  54. package/dist/schemas/workflowPi.mjs +2 -0
  55. package/dist/schemas/workflowPi.mjs.map +1 -0
  56. package/dist/services/workflow/backgroundWork.cjs.map +1 -0
  57. package/dist/services/workflow/backgroundWork.mjs.map +1 -0
  58. package/dist/services/workflow/workflowNarration.cjs +2 -0
  59. package/dist/services/workflow/workflowNarration.cjs.map +1 -0
  60. package/dist/services/workflow/workflowNarration.mjs +2 -0
  61. package/dist/services/workflow/workflowNarration.mjs.map +1 -0
  62. package/dist/tui/workflow/doomOverlay.cjs.map +1 -0
  63. package/dist/tui/workflow/doomOverlay.mjs.map +1 -0
  64. package/dist/tui/workflow/overlayText.cjs.map +1 -0
  65. package/dist/tui/workflow/overlayText.mjs.map +1 -0
  66. package/dist/tui/workflow/rendering.cjs.map +1 -0
  67. package/dist/tui/workflow/rendering.mjs.map +1 -0
  68. package/dist/tui/workflow/workflowInspector.cjs +2 -0
  69. package/dist/{workflow/workflowInspector.mjs.map → tui/workflow/workflowInspector.cjs.map} +1 -1
  70. package/dist/tui/workflow/workflowInspector.mjs +2 -0
  71. package/dist/tui/workflow/workflowInspector.mjs.map +1 -0
  72. package/dist/tui/workflow/workflowOverlay.cjs.map +1 -0
  73. package/dist/tui/workflow/workflowOverlay.mjs.map +1 -0
  74. package/dist/tui/workflow/workflowPicker.cjs.map +1 -0
  75. package/dist/tui/workflow/workflowPicker.mjs.map +1 -0
  76. package/dist/tui/workflow/workflowProgressOverlay.cjs +2 -0
  77. package/dist/tui/workflow/workflowProgressOverlay.cjs.map +1 -0
  78. package/dist/tui/workflow/workflowProgressOverlay.mjs +2 -0
  79. package/dist/tui/workflow/workflowProgressOverlay.mjs.map +1 -0
  80. package/dist/tui/workflow/workflowPrompt.cjs.map +1 -0
  81. package/dist/tui/workflow/workflowPrompt.mjs.map +1 -0
  82. package/dist/tui/workflow/workflowRunPanel.cjs.map +1 -0
  83. package/dist/tui/workflow/workflowRunPanel.mjs.map +1 -0
  84. package/dist/tui/workflow/workflowStatusRow.cjs +2 -0
  85. package/dist/tui/workflow/workflowStatusRow.cjs.map +1 -0
  86. package/dist/tui/workflow/workflowStatusRow.mjs +2 -0
  87. package/dist/tui/workflow/workflowStatusRow.mjs.map +1 -0
  88. package/dist/tui/workflow/workflowStepMessage.cjs +2 -0
  89. package/dist/tui/workflow/workflowStepMessage.cjs.map +1 -0
  90. package/dist/tui/workflow/workflowStepMessage.mjs +2 -0
  91. package/dist/tui/workflow/workflowStepMessage.mjs.map +1 -0
  92. package/dist/tui/workflow/workflowToolRender.cjs +7 -0
  93. package/dist/tui/workflow/workflowToolRender.cjs.map +1 -0
  94. package/dist/tui/workflow/workflowToolRender.mjs +7 -0
  95. package/dist/tui/workflow/workflowToolRender.mjs.map +1 -0
  96. package/dist/{types.cjs → types/index.cjs} +1 -1
  97. package/dist/types/index.cjs.map +1 -0
  98. package/dist/{types.mjs → types/index.mjs} +1 -1
  99. package/dist/types/index.mjs.map +1 -0
  100. package/llms.txt +8 -0
  101. package/package.json +20 -11
  102. package/dist/dispatcher.cjs.map +0 -1
  103. package/dist/dispatcher.d.cts.map +0 -1
  104. package/dist/dispatcher.d.mts.map +0 -1
  105. package/dist/dispatcher.mjs.map +0 -1
  106. package/dist/extension.cjs +0 -2
  107. package/dist/extension.cjs.map +0 -1
  108. package/dist/extension.d.cts.map +0 -1
  109. package/dist/extension.d.mts.map +0 -1
  110. package/dist/extension.mjs +0 -2
  111. package/dist/extension.mjs.map +0 -1
  112. package/dist/leader.cjs +0 -2
  113. package/dist/leader.cjs.map +0 -1
  114. package/dist/leader.mjs +0 -2
  115. package/dist/leader.mjs.map +0 -1
  116. package/dist/types.cjs.map +0 -1
  117. package/dist/types.mjs.map +0 -1
  118. package/dist/workflow/backgroundWork.cjs.map +0 -1
  119. package/dist/workflow/backgroundWork.mjs.map +0 -1
  120. package/dist/workflow/doomOverlay.cjs.map +0 -1
  121. package/dist/workflow/doomOverlay.mjs.map +0 -1
  122. package/dist/workflow/overlayText.cjs.map +0 -1
  123. package/dist/workflow/overlayText.mjs.map +0 -1
  124. package/dist/workflow/piExtension.cjs +0 -11
  125. package/dist/workflow/piExtension.cjs.map +0 -1
  126. package/dist/workflow/piExtension.mjs +0 -11
  127. package/dist/workflow/piExtension.mjs.map +0 -1
  128. package/dist/workflow/piToolBridge.cjs.map +0 -1
  129. package/dist/workflow/piToolBridge.mjs.map +0 -1
  130. package/dist/workflow/piTools.cjs +0 -4
  131. package/dist/workflow/piTools.cjs.map +0 -1
  132. package/dist/workflow/piTools.mjs +0 -4
  133. package/dist/workflow/piTools.mjs.map +0 -1
  134. package/dist/workflow/rendering.cjs.map +0 -1
  135. package/dist/workflow/rendering.mjs.map +0 -1
  136. package/dist/workflow/workflowFooterStatus.cjs +0 -2
  137. package/dist/workflow/workflowFooterStatus.cjs.map +0 -1
  138. package/dist/workflow/workflowFooterStatus.mjs +0 -2
  139. package/dist/workflow/workflowFooterStatus.mjs.map +0 -1
  140. package/dist/workflow/workflowInspector.cjs +0 -2
  141. package/dist/workflow/workflowInspector.cjs.map +0 -1
  142. package/dist/workflow/workflowInspector.mjs +0 -2
  143. package/dist/workflow/workflowLauncher.cjs +0 -2
  144. package/dist/workflow/workflowLauncher.cjs.map +0 -1
  145. package/dist/workflow/workflowLauncher.mjs +0 -2
  146. package/dist/workflow/workflowLauncher.mjs.map +0 -1
  147. package/dist/workflow/workflowOverlay.cjs.map +0 -1
  148. package/dist/workflow/workflowOverlay.mjs.map +0 -1
  149. package/dist/workflow/workflowPicker.cjs.map +0 -1
  150. package/dist/workflow/workflowPicker.mjs.map +0 -1
  151. package/dist/workflow/workflowPrompt.cjs.map +0 -1
  152. package/dist/workflow/workflowPrompt.mjs.map +0 -1
  153. package/dist/workflow/workflowRunPanel.cjs.map +0 -1
  154. package/dist/workflow/workflowRunPanel.mjs.map +0 -1
  155. package/dist/workflow/workflowStatusRow.cjs +0 -2
  156. package/dist/workflow/workflowStatusRow.cjs.map +0 -1
  157. package/dist/workflow/workflowStatusRow.mjs +0 -2
  158. package/dist/workflow/workflowStatusRow.mjs.map +0 -1
  159. package/dist/workflow/workflowStepMessage.cjs +0 -2
  160. package/dist/workflow/workflowStepMessage.cjs.map +0 -1
  161. package/dist/workflow/workflowStepMessage.mjs +0 -2
  162. package/dist/workflow/workflowStepMessage.mjs.map +0 -1
  163. /package/dist/{dispatcher.cjs → adapters/pi/dispatcher.cjs} +0 -0
  164. /package/dist/{dispatcher.mjs → adapters/pi/dispatcher.mjs} +0 -0
  165. /package/dist/{workflow → services/workflow}/backgroundWork.cjs +0 -0
  166. /package/dist/{workflow → services/workflow}/backgroundWork.mjs +0 -0
  167. /package/dist/{workflow → tui/workflow}/doomOverlay.cjs +0 -0
  168. /package/dist/{workflow → tui/workflow}/doomOverlay.mjs +0 -0
  169. /package/dist/{workflow → tui/workflow}/overlayText.cjs +0 -0
  170. /package/dist/{workflow → tui/workflow}/overlayText.mjs +0 -0
  171. /package/dist/{workflow → tui/workflow}/rendering.cjs +0 -0
  172. /package/dist/{workflow → tui/workflow}/rendering.mjs +0 -0
  173. /package/dist/{workflow → tui/workflow}/workflowOverlay.cjs +0 -0
  174. /package/dist/{workflow → tui/workflow}/workflowOverlay.mjs +0 -0
  175. /package/dist/{workflow → tui/workflow}/workflowPicker.cjs +0 -0
  176. /package/dist/{workflow → tui/workflow}/workflowPicker.mjs +0 -0
  177. /package/dist/{workflow → tui/workflow}/workflowPrompt.cjs +0 -0
  178. /package/dist/{workflow → tui/workflow}/workflowPrompt.mjs +0 -0
  179. /package/dist/{workflow → tui/workflow}/workflowRunPanel.cjs +0 -0
  180. /package/dist/{workflow → tui/workflow}/workflowRunPanel.mjs +0 -0
@@ -1 +1 @@
1
- {"version":3,"file":"workflowInspector.mjs","names":[],"sources":["../../src/workflow/workflowInspector.ts"],"sourcesContent":["/**\n * Workflow Space: the `SPC w l` overlay.\n *\n * A doom overlay rather than a hand-rolled frame: the roster owns the left\n * third, the run under the cursor owns the right two thirds, and the chrome is\n * the same frame, breadcrumb and key legend every other doom surface draws.\n *\n * Rendering is pure -- `renderBody(width, height)` reads the last polled\n * snapshot and returns lines with no side effects -- so the surface can be\n * asserted as text without a live terminal.\n */\n\nimport type { Theme } from '@earendil-works/pi-coding-agent';\nimport {\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n DOOM_FULLSCREEN_UI_OPTIONS,\n} from './doomOverlay.ts';\nimport { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport { currentWorkflowPosition, type WorkflowProgressJob } from '@agimon-ai/workflow-mcp';\nimport { humanizeDuration } from './workflowStatusRow';\n\nexport const WORKFLOW_INSPECTOR_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nexport interface WorkflowInspectorSelection {\n runId?: string;\n runKey: string;\n startedAt: string;\n workspace: string;\n}\n\nexport interface WorkflowInspectorItem extends WorkflowInspectorSelection {\n displayName: string;\n executionState?: string;\n jobs: WorkflowProgressJob[];\n key: string;\n output: string[];\n}\n\nexport interface WorkflowInspectorSource {\n list(): Promise<WorkflowInspectorItem[]>;\n output(item: WorkflowInspectorItem): Promise<string[]>;\n}\n\nexport interface WorkflowInspectorOptions {\n initialKey?: string;\n refreshMs?: number;\n}\n\nconst TITLE = 'WORKFLOW SPACE';\nconst BREADCRUMB = 'SPC › w / workflows › l / manage';\nconst ROSTER_HEADING = 'RUNNING · this session';\nconst EMPTY_ROSTER = 'No active workflows';\nconst PROGRESS_HEADING = 'PROGRESS';\nconst OUTPUT_HEADING = 'OUTPUT · newest last';\nconst NO_SELECTION = 'Select a workflow to inspect its jobs and output.';\nconst RUNNING_STATE = 'running';\nconst PAUSED_STATE = 'paused';\nconst FOOTER = '↑↓/jk select · PgUp/PgDn page · enter open · esc close';\nconst FOOTER_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓/jk', 'select'],\n ['PgUp/PgDn', 'page'],\n ['enter', 'open'],\n ['esc', 'close'],\n];\n\nconst DEFAULT_REFRESH_MS = 750;\nconst SELECTION_MARKER = '›';\nconst ELLIPSIS = '…';\n/** pi-tui brackets a truncation ellipsis with this; see `fit`. */\nconst HARD_RESET = '\\x1b[0m';\n/** A third for the roster, two thirds for the run being read. */\nconst LIST_PANE_RATIO = 1 / 3;\n/** One blank column each side of the divider so neither pane touches it. */\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 18;\n/** Below this the roster cannot hold a readable name, so the detail takes the body. */\nconst MIN_TWO_PANE_WIDTH = 48;\n/** Each run occupies a name row and a state row beneath it. */\nconst ROWS_PER_ITEM = 2;\n/** Hanging indent for the state row: clears the marker and the glyph. */\nconst META_INDENT = 4;\n/** Rows the roster heading and its spacer take. */\nconst ROSTER_CHROME_ROWS = 2;\n/** The output tail keeps at least this many rows, or it is not worth a divider. */\nconst MIN_OUTPUT_ROWS = 2;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\n/**\n * Truncates and pads to an exact column count.\n *\n * `truncateToWidth` wraps its ellipsis in a hard `\\x1b[0m`, which would strip a\n * row's background for everything after a clipped name. The theme closes its own\n * colours with `\\x1b[39m` and `\\x1b[49m`, so the injected resets are dropped.\n */\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS).replaceAll(HARD_RESET, '');\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction rightAligned(left: string, right: string, width: number): string {\n const rightWidth = visibleWidth(right);\n const leftWidth = Math.max(0, width - rightWidth - 1);\n return fit(left, leftWidth) + ' '.repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);\n}\n\nfunction jobGlyph(status: string): string {\n return status === 'completed' ? '✓' : status === 'failed' ? '✗' : '●';\n}\n\nfunction jobColour(status: string): ThemeColor {\n return status === 'completed' ? 'success' : status === 'failed' ? 'error' : 'accent';\n}\n\nfunction progressLines(item: WorkflowInspectorItem, width: number, theme: Theme): string[] {\n const lines: string[] = [];\n for (const job of item.jobs) {\n const position = job.total ? ` · ${(job.index ?? 0) + 1}/${job.total}` : '';\n lines.push(\n truncateToWidth(\n theme.fg(jobColour(job.status), `${jobGlyph(job.status)} ${job.name}`) + theme.fg('dim', position),\n width,\n ELLIPSIS,\n ),\n );\n // Only the running job's steps: every finished job's steps would push the\n // live one off the pane, which is the one thing worth watching.\n if (job.status !== RUNNING_STATE) continue;\n for (const step of job.steps) {\n const stepColour: ThemeColor =\n step.status === 'failed' ? 'error' : step.status === RUNNING_STATE ? 'muted' : 'dim';\n lines.push(truncateToWidth(theme.fg(stepColour, ` ${jobGlyph(step.status)} ${step.name}`), width, ELLIPSIS));\n }\n }\n return lines;\n}\n\nfunction detailLines(item: WorkflowInspectorItem | undefined, width: number, height: number, theme: Theme): string[] {\n if (!item) return [theme.fg('dim', NO_SELECTION)];\n\n const position = currentWorkflowPosition(item.jobs);\n const elapsed = humanizeDuration(Math.max(0, Date.now() - Date.parse(item.startedAt)));\n const executionState = item.executionState ?? RUNNING_STATE;\n const stateColour: ThemeColor = executionState === PAUSED_STATE ? 'warning' : 'dim';\n const lines = [\n rightAligned(\n `${theme.fg('accent', '●')} ${theme.bold(item.displayName)}`,\n theme.fg(stateColour, executionState),\n width,\n ),\n truncateToWidth(theme.fg('dim', `${item.runKey} · ${item.workspace} · ${elapsed}`), width, ELLIPSIS),\n position\n ? truncateToWidth(\n `${theme.fg('accent', position.job)}${position.step ? theme.fg('muted', ` · ${position.step}`) : ''}`,\n width,\n ELLIPSIS,\n )\n : theme.fg('dim', 'starting'),\n '',\n theme.fg('dim', PROGRESS_HEADING),\n ];\n\n // The output tail claims its rows first: progress is a list that can be paged\n // to by opening the run, while the tail is the only live signal on screen.\n const reservedOutput = item.output.length > 0 ? MIN_OUTPUT_ROWS + 1 : 0;\n const progressRoom = Math.max(0, height - lines.length - reservedOutput);\n lines.push(...progressLines(item, width, theme).slice(0, progressRoom));\n\n if (item.output.length > 0 && height - lines.length >= MIN_OUTPUT_ROWS) {\n lines.push('', theme.fg('dim', OUTPUT_HEADING));\n const outputRoom = Math.max(0, height - lines.length);\n lines.push(...item.output.slice(-outputRoom).map((line) => truncateToWidth(line, width, ELLIPSIS)));\n }\n\n return lines.slice(0, height);\n}\n\nexport class WorkflowInspectorComponent extends DoomOverlay {\n private bodyHeight = 8;\n private completed = false;\n private error: string | undefined;\n private items: WorkflowInspectorItem[] = [];\n private refreshActive = false;\n private selected = 0;\n private selectedKey: string | undefined;\n private readonly timer: ReturnType<typeof setInterval>;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly source: WorkflowInspectorSource,\n private readonly done: (selection: WorkflowInspectorSelection | undefined) => void,\n options: WorkflowInspectorOptions = {},\n ) {\n super(tui, theme);\n this.selectedKey = options.initialKey;\n void this.refresh();\n this.timer = setInterval(() => void this.refresh(), options.refreshMs ?? DEFAULT_REFRESH_MS);\n this.timer.unref?.();\n }\n\n private async refresh(): Promise<void> {\n if (this.refreshActive || this.completed) return;\n this.refreshActive = true;\n try {\n const previousKey = this.items[this.selected]?.key ?? this.selectedKey;\n const items = await this.source.list();\n if (this.completed) return;\n const preserved = previousKey ? items.findIndex((item) => item.key === previousKey) : -1;\n this.items = items;\n this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, items.length - 1));\n this.selectedKey = items[this.selected]?.key;\n const selected = items[this.selected];\n if (selected) selected.output = await this.source.output(selected);\n this.error = undefined;\n if (!this.completed) this.tui.requestRender();\n } catch (cause) {\n this.error = cause instanceof Error ? cause.message : String(cause);\n if (!this.completed) this.tui.requestRender();\n } finally {\n this.refreshActive = false;\n }\n }\n\n private move(delta: number): void {\n if (this.items.length === 0) return;\n this.selected = Math.max(0, Math.min(this.items.length - 1, this.selected + delta));\n this.selectedKey = this.items[this.selected]?.key;\n void this.refreshSelectedOutput();\n this.tui.requestRender();\n }\n\n private async refreshSelectedOutput(): Promise<void> {\n const selected = this.items[this.selected];\n if (!selected || this.completed) return;\n try {\n selected.output = await this.source.output(selected);\n this.error = undefined;\n } catch (cause) {\n this.error = cause instanceof Error ? cause.message : String(cause);\n }\n if (!this.completed && selected.key === this.selectedKey) this.tui.requestRender();\n }\n\n private finish(selection: WorkflowInspectorSelection | undefined): void {\n if (this.completed) return;\n this.completed = true;\n clearInterval(this.timer);\n this.done(selection);\n }\n\n close(): void {\n this.finish(undefined);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.finish(undefined);\n return;\n }\n if (matchesKey(data, 'up') || data === 'k') {\n this.move(-1);\n return;\n }\n if (matchesKey(data, 'down') || data === 'j') {\n this.move(1);\n return;\n }\n if (matchesKey(data, 'pageUp')) {\n this.move(-this.rosterCapacity());\n return;\n }\n if (matchesKey(data, 'pageDown')) {\n this.move(this.rosterCapacity());\n return;\n }\n if (matchesKey(data, 'enter')) {\n const selected = this.items[this.selected];\n if (selected) {\n this.finish({\n ...(selected.runId ? { runId: selected.runId } : {}),\n runKey: selected.runKey,\n startedAt: selected.startedAt,\n workspace: selected.workspace,\n });\n }\n }\n }\n\n /** Runs the roster can draw, which is also what a page key moves by. */\n private rosterCapacity(): number {\n return Math.max(1, Math.floor((this.bodyHeight - ROSTER_CHROME_ROWS) / ROWS_PER_ITEM));\n }\n\n private current(): WorkflowInspectorItem | undefined {\n if (this.items.length === 0) return undefined;\n return this.items[Math.min(this.selected, this.items.length - 1)];\n }\n\n protected getChrome(): DoomOverlayChrome {\n const selected = this.current();\n const summary = selected\n ? `${selected.displayName} · ${selected.executionState ?? RUNNING_STATE}`\n : 'no active workflows';\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${this.items.length} running · ${summary}`,\n footer: FOOTER,\n footerHints: FOOTER_HINTS,\n footerRight:\n this.items.length > 0 ? `${Math.min(this.selected, this.items.length - 1) + 1}/${this.items.length}` : 'empty',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n this.bodyHeight = height;\n const detailFor = (paneWidth: number): string[] =>\n this.error\n ? [this.theme.fg('warning', `Unable to refresh workflows: ${this.error}`)]\n : detailLines(this.current(), paneWidth, height, this.theme);\n\n // Too narrow for two useful columns: the detail takes the whole body rather\n // than shredding both panes into stubs.\n if (width < MIN_TWO_PANE_WIDTH) return detailFor(width).slice(0, height);\n\n const leftWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * LIST_PANE_RATIO));\n const rightWidth = Math.max(1, width - leftWidth - 1);\n const leftContent = Math.max(1, leftWidth - PANE_GUTTER);\n const rightContent = Math.max(1, rightWidth - PANE_GUTTER);\n\n const left = this.rosterLines(leftContent);\n const right = detailFor(rightContent);\n const divider = this.theme.fg('borderMuted', '│');\n return Array.from({ length: height }, (_, index) => {\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fit(right[index] ?? '', rightContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n }\n\n private rosterLines(width: number): string[] {\n const lines = [this.theme.bold(ROSTER_HEADING), ''];\n if (this.items.length === 0) {\n lines.push(this.theme.fg('dim', EMPTY_ROSTER));\n return lines;\n }\n\n // Two rows per run: at a third of the width a single row left the name with\n // a handful of columns once the stage took its share.\n const budget = this.rosterCapacity();\n const active = Math.min(this.selected, this.items.length - 1);\n const start = Math.max(0, Math.min(active - budget + 1, Math.max(0, this.items.length - budget)));\n for (const [offset, item] of this.items.slice(start, start + budget).entries()) {\n const current = start + offset === active;\n const paused = item.executionState === PAUSED_STATE;\n const marker = current ? this.theme.fg('accent', SELECTION_MARKER) : ' ';\n const glyph = this.theme.fg(paused ? 'warning' : 'accent', '●');\n const name = current ? this.theme.bold(item.displayName) : item.displayName;\n const stage = paused ? PAUSED_STATE : (currentWorkflowPosition(item.jobs)?.job ?? 'starting');\n const heading = `${marker} ${glyph} ${name}`;\n const meta = `${' '.repeat(META_INDENT)}${this.theme.fg(paused ? 'warning' : 'dim', stage)}`;\n const background: ThemeBg = current ? 'selectedBg' : 'userMessageBg';\n for (const row of [heading, meta]) lines.push(this.theme.bg(background, fit(row, width)));\n }\n return lines;\n }\n\n /** Overridden: a repaint request is also the cue to re-read the registry. */\n invalidate(): void {\n void this.refresh();\n }\n\n dispose(): void {\n if (this.completed) return;\n this.completed = true;\n clearInterval(this.timer);\n }\n}\n"],"mappings":"iUAwBkD,EAA2B,eA2B7E,MAOM,EAAgB,UAChB,EAAe,SAEf,EAAuD,CAC3D,CAAC,QAAS,QAAQ,EAClB,CAAC,YAAa,MAAM,EACpB,CAAC,QAAS,MAAM,EAChB,CAAC,MAAO,OAAO,CACjB,EAkCA,SAAS,EAAI,EAAc,EAAuB,CAChD,IAAM,EAAU,EAAgB,EAAM,KAAK,IAAI,EAAG,CAAK,EAAG,GAAQ,CAAC,CAAC,WAAW,UAAY,EAAE,EAC7F,OAAO,EAAU,IAAI,OAAO,KAAK,IAAI,EAAG,EAAQ,EAAa,CAAO,CAAC,CAAC,CACxE,CAEA,SAAS,EAAa,EAAc,EAAe,EAAuB,CACxE,IAAM,EAAa,EAAa,CAAK,EAC/B,EAAY,KAAK,IAAI,EAAG,EAAQ,EAAa,CAAC,EACpD,OAAO,EAAI,EAAM,CAAS,EAAI,IAAI,OAAO,KAAK,IAAI,EAAG,EAAQ,EAAY,CAAU,CAAC,EAAI,EAAI,EAAO,CAAU,CAC/G,CAEA,SAAS,EAAS,EAAwB,CACxC,OAAO,IAAW,YAAc,IAAM,IAAW,SAAW,IAAM,GACpE,CAEA,SAAS,EAAU,EAA4B,CAC7C,OAAO,IAAW,YAAc,UAAY,IAAW,SAAW,QAAU,QAC9E,CAEA,SAAS,EAAc,EAA6B,EAAe,EAAwB,CACzF,IAAM,EAAkB,CAAC,EACzB,IAAK,IAAM,KAAO,EAAK,KAAM,CAC3B,IAAM,EAAW,EAAI,MAAQ,OAAO,EAAI,OAAS,GAAK,EAAE,GAAG,EAAI,QAAU,GACzE,KAAM,KACJ,EACE,EAAM,GAAG,EAAU,EAAI,MAAM,EAAG,GAAG,EAAS,EAAI,MAAM,EAAE,GAAG,EAAI,MAAM,EAAI,EAAM,GAAG,MAAO,CAAQ,EACjG,EACA,GACF,CACF,EAGI,EAAI,SAAW,EACnB,IAAK,IAAM,KAAQ,EAAI,MAAO,CAC5B,IAAM,EACJ,EAAK,SAAW,SAAW,QAAU,EAAK,SAAW,EAAgB,QAAU,MACjF,EAAM,KAAK,EAAgB,EAAM,GAAG,EAAY,MAAM,EAAS,EAAK,MAAM,EAAE,GAAG,EAAK,MAAM,EAAG,EAAO,GAAQ,CAAC,CAC/G,CACF,CACA,OAAO,CACT,CAEA,SAAS,EAAY,EAAyC,EAAe,EAAgB,EAAwB,CACnH,GAAI,CAAC,EAAM,MAAO,CAAC,EAAM,GAAG,MAAO,mDAAY,CAAC,EAEhD,IAAM,EAAW,EAAwB,EAAK,IAAI,EAC5C,EAAU,EAAiB,KAAK,IAAI,EAAG,KAAK,IAAI,EAAI,KAAK,MAAM,EAAK,SAAS,CAAC,CAAC,EAC/E,EAAiB,EAAK,gBAAkB,EACxC,EAA0B,IAAmB,EAAe,UAAY,MACxE,EAAQ,CACZ,EACE,GAAG,EAAM,GAAG,SAAU,GAAG,EAAE,GAAG,EAAM,KAAK,EAAK,WAAW,IACzD,EAAM,GAAG,EAAa,CAAc,EACpC,CACF,EACA,EAAgB,EAAM,GAAG,MAAO,GAAG,EAAK,OAAO,KAAK,EAAK,UAAU,KAAK,GAAS,EAAG,EAAO,GAAQ,EACnG,EACI,EACE,GAAG,EAAM,GAAG,SAAU,EAAS,GAAG,IAAI,EAAS,KAAO,EAAM,GAAG,QAAS,MAAM,EAAS,MAAM,EAAI,KACjG,EACA,GACF,EACA,EAAM,GAAG,MAAO,UAAU,EAC9B,GACA,EAAM,GAAG,MAAO,UAAgB,CAClC,EAIM,EAAiB,EAAK,OAAO,OAAS,EAAI,EAAsB,EAChE,EAAe,KAAK,IAAI,EAAG,EAAS,EAAM,OAAS,CAAc,EAGvE,GAFA,EAAM,KAAK,GAAG,EAAc,EAAM,EAAO,CAAK,CAAC,CAAC,MAAM,EAAG,CAAY,CAAC,EAElE,EAAK,OAAO,OAAS,GAAK,EAAS,EAAM,QAAU,EAAiB,CACtE,EAAM,KAAK,GAAI,EAAM,GAAG,MAAO,sBAAc,CAAC,EAC9C,IAAM,EAAa,KAAK,IAAI,EAAG,EAAS,EAAM,MAAM,EACpD,EAAM,KAAK,GAAG,EAAK,OAAO,MAAM,CAAC,CAAU,CAAC,CAAC,IAAK,GAAS,EAAgB,EAAM,EAAO,GAAQ,CAAC,CAAC,CACpG,CAEA,OAAO,EAAM,MAAM,EAAG,CAAM,CAC9B,CAEA,IAAa,EAAb,cAAgD,CAAY,CAavC,OACA,KAbnB,WAAqB,EACrB,UAAoB,GACpB,MACA,MAAyC,CAAC,EAC1C,cAAwB,GACxB,SAAmB,EACnB,YACA,MAEA,YACE,EACA,EACA,EACA,EACA,EAAoC,CAAC,EACrC,CACA,MAAM,EAAK,CAAK,EAJC,KAAA,OAAA,EACA,KAAA,KAAA,EAIjB,KAAK,YAAc,EAAQ,WAC3B,KAAU,QAAQ,EAClB,KAAK,MAAQ,gBAAkB,KAAK,KAAK,QAAQ,EAAG,EAAQ,WAAa,GAAkB,EAC3F,KAAK,MAAM,QAAQ,CACrB,CAEA,MAAc,SAAyB,CACjC,UAAK,eAAiB,KAAK,WAC/B,MAAK,cAAgB,GACrB,GAAI,CACF,IAAM,EAAc,KAAK,MAAM,KAAK,SAAS,EAAE,KAAO,KAAK,YACrD,EAAQ,MAAM,KAAK,OAAO,KAAK,EACrC,GAAI,KAAK,UAAW,OACpB,IAAM,EAAY,EAAc,EAAM,UAAW,GAAS,EAAK,MAAQ,CAAW,EAAI,GACtF,KAAK,MAAQ,EACb,KAAK,SAAW,GAAa,EAAI,EAAY,KAAK,IAAI,KAAK,SAAU,KAAK,IAAI,EAAG,EAAM,OAAS,CAAC,CAAC,EAClG,KAAK,YAAc,EAAM,KAAK,SAAS,EAAE,IACzC,IAAM,EAAW,EAAM,KAAK,UACxB,IAAU,EAAS,OAAS,MAAM,KAAK,OAAO,OAAO,CAAQ,GACjE,KAAK,MAAQ,IAAA,GACR,KAAK,WAAW,KAAK,IAAI,cAAc,CAC9C,OAAS,EAAO,CACd,KAAK,MAAQ,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,EAC7D,KAAK,WAAW,KAAK,IAAI,cAAc,CAC9C,QAAU,CACR,KAAK,cAAgB,EACvB,CAlBqB,CAmBvB,CAEA,KAAa,EAAqB,CAC5B,KAAK,MAAM,SAAW,IAC1B,KAAK,SAAW,KAAK,IAAI,EAAG,KAAK,IAAI,KAAK,MAAM,OAAS,EAAG,KAAK,SAAW,CAAK,CAAC,EAClF,KAAK,YAAc,KAAK,MAAM,KAAK,SAAS,EAAE,IAC9C,KAAU,sBAAsB,EAChC,KAAK,IAAI,cAAc,EACzB,CAEA,MAAc,uBAAuC,CACnD,IAAM,EAAW,KAAK,MAAM,KAAK,UAC7B,MAAC,GAAY,KAAK,WACtB,IAAI,CACF,EAAS,OAAS,MAAM,KAAK,OAAO,OAAO,CAAQ,EACnD,KAAK,MAAQ,IAAA,EACf,OAAS,EAAO,CACd,KAAK,MAAQ,aAAiB,MAAQ,EAAM,QAAU,OAAO,CAAK,CACpE,CACI,CAAC,KAAK,WAAa,EAAS,MAAQ,KAAK,aAAa,KAAK,IAAI,cAAc,CADjF,CAEF,CAEA,OAAe,EAAyD,CAClE,KAAK,YACT,KAAK,UAAY,GACjB,cAAc,KAAK,KAAK,EACxB,KAAK,KAAK,CAAS,EACrB,CAEA,OAAc,CACZ,KAAK,OAAO,IAAA,EAAS,CACvB,CAEA,YAAY,EAAoB,CAC9B,GAAI,EAAW,EAAM,QAAQ,GAAK,EAAW,EAAM,QAAQ,EAAG,CAC5D,KAAK,OAAO,IAAA,EAAS,EACrB,MACF,CACA,GAAI,EAAW,EAAM,IAAI,GAAK,IAAS,IAAK,CAC1C,KAAK,KAAK,EAAE,EACZ,MACF,CACA,GAAI,EAAW,EAAM,MAAM,GAAK,IAAS,IAAK,CAC5C,KAAK,KAAK,CAAC,EACX,MACF,CACA,GAAI,EAAW,EAAM,QAAQ,EAAG,CAC9B,KAAK,KAAK,CAAC,KAAK,eAAe,CAAC,EAChC,MACF,CACA,GAAI,EAAW,EAAM,UAAU,EAAG,CAChC,KAAK,KAAK,KAAK,eAAe,CAAC,EAC/B,MACF,CACA,GAAI,EAAW,EAAM,OAAO,EAAG,CAC7B,IAAM,EAAW,KAAK,MAAM,KAAK,UAC7B,GACF,KAAK,OAAO,CACV,GAAI,EAAS,MAAQ,CAAE,MAAO,EAAS,KAAM,EAAI,CAAC,EAClD,OAAQ,EAAS,OACjB,UAAW,EAAS,UACpB,UAAW,EAAS,SACtB,CAAC,CAEL,CACF,CAGA,gBAAiC,CAC/B,OAAO,KAAK,IAAI,EAAG,KAAK,OAAO,KAAK,WAAa,GAAsB,CAAa,CAAC,CACvF,CAEA,SAAqD,CAC/C,QAAK,MAAM,SAAW,EAC1B,OAAO,KAAK,MAAM,KAAK,IAAI,KAAK,SAAU,KAAK,MAAM,OAAS,CAAC,EACjE,CAEA,WAAyC,CACvC,IAAM,EAAW,KAAK,QAAQ,EACxB,EAAU,EACZ,GAAG,EAAS,YAAY,KAAK,EAAS,gBAAkB,IACxD,sBACJ,MAAO,CACL,MAAO,iBACP,OAAQ,EACR,WAAY,mCACZ,YAAa,GAAG,KAAK,MAAM,OAAO,aAAa,IAC/C,OAAQ,yDACR,YAAa,EACb,YACE,KAAK,MAAM,OAAS,EAAI,GAAG,KAAK,IAAI,KAAK,SAAU,KAAK,MAAM,OAAS,CAAC,EAAI,EAAE,GAAG,KAAK,MAAM,SAAW,OAC3G,CACF,CAEA,WAAqB,EAAe,EAA0B,CAC5D,KAAK,WAAa,EAClB,IAAM,EAAa,GACjB,KAAK,MACD,CAAC,KAAK,MAAM,GAAG,UAAW,gCAAgC,KAAK,OAAO,CAAC,EACvE,EAAY,KAAK,QAAQ,EAAG,EAAW,EAAQ,KAAK,KAAK,EAI/D,GAAI,EAAQ,GAAoB,OAAO,EAAU,CAAK,CAAC,CAAC,MAAM,EAAG,CAAM,EAEvE,IAAM,EAAY,KAAK,IAAI,GAAgB,KAAK,OAAO,EAAQ,GAAK,iBAAe,CAAC,EAC9E,EAAa,KAAK,IAAI,EAAG,EAAQ,EAAY,CAAC,EAC9C,EAAc,KAAK,IAAI,EAAG,EAAY,CAAW,EACjD,EAAe,KAAK,IAAI,EAAG,EAAa,CAAW,EAEnD,EAAO,KAAK,YAAY,CAAW,EACnC,EAAQ,EAAU,CAAY,EAC9B,EAAU,KAAK,MAAM,GAAG,cAAe,GAAG,EAChD,OAAO,MAAM,KAAK,CAAE,OAAQ,CAAO,GAAI,EAAG,IAAU,CAClD,IAAM,EAAM,GAAG,EAAI,EAAK,IAAU,GAAI,CAAW,EAAE,GAAG,EAAQ,GAAG,EAAI,EAAM,IAAU,GAAI,CAAY,IACrG,OAAO,EAAgB,EAAK,EAAO,GAAQ,CAC7C,CAAC,CACH,CAEA,YAAoB,EAAyB,CAC3C,IAAM,EAAQ,CAAC,KAAK,MAAM,KAAK,wBAAc,EAAG,EAAE,EAClD,GAAI,KAAK,MAAM,SAAW,EAExB,OADA,EAAM,KAAK,KAAK,MAAM,GAAG,MAAO,qBAAY,CAAC,EACtC,EAKT,IAAM,EAAS,KAAK,eAAe,EAC7B,EAAS,KAAK,IAAI,KAAK,SAAU,KAAK,MAAM,OAAS,CAAC,EACtD,EAAQ,KAAK,IAAI,EAAG,KAAK,IAAI,EAAS,EAAS,EAAG,KAAK,IAAI,EAAG,KAAK,MAAM,OAAS,CAAM,CAAC,CAAC,EAChG,IAAK,GAAM,CAAC,EAAQ,KAAS,KAAK,MAAM,MAAM,EAAO,EAAQ,CAAM,CAAC,CAAC,QAAQ,EAAG,CAC9E,IAAM,EAAU,EAAQ,IAAW,EAC7B,EAAS,EAAK,iBAAmB,EACjC,EAAS,EAAU,KAAK,MAAM,GAAG,SAAU,GAAgB,EAAI,IAC/D,EAAQ,KAAK,MAAM,GAAG,EAAS,UAAY,SAAU,GAAG,EACxD,EAAO,EAAU,KAAK,MAAM,KAAK,EAAK,WAAW,EAAI,EAAK,YAC1D,EAAQ,EAAS,EAAgB,EAAwB,EAAK,IAAI,CAAC,EAAE,KAAO,WAC5E,EAAU,GAAG,EAAO,GAAG,EAAM,GAAG,IAChC,EAAO,GAAG,IAAI,OAAO,CAAW,IAAI,KAAK,MAAM,GAAG,EAAS,UAAY,MAAO,CAAK,IACnF,EAAsB,EAAU,aAAe,gBACrD,IAAK,IAAM,IAAO,CAAC,EAAS,CAAI,EAAG,EAAM,KAAK,KAAK,MAAM,GAAG,EAAY,EAAI,EAAK,CAAK,CAAC,CAAC,CAC1F,CACA,OAAO,CACT,CAGA,YAAmB,CACjB,KAAU,QAAQ,CACpB,CAEA,SAAgB,CACV,KAAK,YACT,KAAK,UAAY,GACjB,cAAc,KAAK,KAAK,EAC1B,CACF"}
1
+ {"version":3,"file":"workflowInspector.cjs","names":["DOOM_FULLSCREEN_UI_OPTIONS","truncateToWidth","visibleWidth","currentWorkflowPosition","humanizeDuration","DoomOverlay","matchesKey","DOOM_OVERLAY_ACCENT"],"sources":["../../../src/tui/workflow/workflowInspector.ts"],"sourcesContent":["/**\n * Workflow Space: the `SPC w l` overlay.\n *\n * A doom overlay rather than a hand-rolled frame: the roster owns the left\n * third, the run under the cursor owns the right two thirds, and the chrome is\n * the same frame, breadcrumb and key legend every other doom surface draws.\n *\n * Rendering is pure -- `renderBody(width, height)` reads the last polled\n * snapshot and returns lines with no side effects -- so the surface can be\n * asserted as text without a live terminal.\n */\n\nimport { currentWorkflowPosition, type WorkflowProgressJob } from '@agimon-ai/workflow-mcp';\nimport type { Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay.ts';\nimport { humanizeDuration } from './workflowStatusRow';\n\nexport const WORKFLOW_INSPECTOR_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nexport interface WorkflowInspectorSelection {\n runId?: string;\n runKey: string;\n startedAt: string;\n workspace: string;\n}\n\nexport interface WorkflowInspectorItem extends WorkflowInspectorSelection {\n displayName: string;\n executionState?: string;\n jobs: WorkflowProgressJob[];\n key: string;\n output: string[];\n}\n\nexport interface WorkflowInspectorSource {\n list(): Promise<WorkflowInspectorItem[]>;\n output(item: WorkflowInspectorItem): Promise<string[]>;\n}\n\nexport interface WorkflowInspectorOptions {\n initialKey?: string;\n refreshMs?: number;\n}\n\nconst TITLE = 'WORKFLOW SPACE';\nconst BREADCRUMB = 'SPC › w / workflows › l / manage';\nconst ROSTER_HEADING = 'RUNNING · this session';\nconst EMPTY_ROSTER = 'No active workflows';\nconst PROGRESS_HEADING = 'PROGRESS';\nconst OUTPUT_HEADING = 'OUTPUT · newest last';\nconst NO_SELECTION = 'Select a workflow to inspect its jobs and output.';\nconst RUNNING_STATE = 'running';\nconst PAUSED_STATE = 'paused';\n/** Mirrors DoomPi's list-navigation legend without adding a runtime doompi-ui dependency. */\nconst LIST_NAVIGATION_KEYS = '↑↓';\nconst FOOTER = `${LIST_NAVIGATION_KEYS} select · PgUp/PgDn page · enter open · esc close`;\nconst FOOTER_HINTS: readonly (readonly [string, string])[] = [\n [LIST_NAVIGATION_KEYS, 'select'],\n ['PgUp/PgDn', 'page'],\n ['enter', 'open'],\n ['esc', 'close'],\n];\n\nconst DEFAULT_REFRESH_MS = 750;\nconst SELECTION_MARKER = '›';\nconst ELLIPSIS = '…';\n/** pi-tui brackets a truncation ellipsis with this; see `fit`. */\nconst HARD_RESET = '\\x1b[0m';\n/** A third for the roster, two thirds for the run being read. */\nconst LIST_PANE_RATIO = 1 / 3;\n/** One blank column each side of the divider so neither pane touches it. */\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 18;\n/** Below this the roster cannot hold a readable name, so the detail takes the body. */\nconst MIN_TWO_PANE_WIDTH = 48;\n/** Each run occupies a name row and a state row beneath it. */\nconst ROWS_PER_ITEM = 2;\n/** Hanging indent for the state row: clears the marker and the glyph. */\nconst META_INDENT = 4;\n/** Rows the roster heading and its spacer take. */\nconst ROSTER_CHROME_ROWS = 2;\n/** The output tail keeps at least this many rows, or it is not worth a divider. */\nconst MIN_OUTPUT_ROWS = 2;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\n/**\n * Truncates and pads to an exact column count.\n *\n * `truncateToWidth` wraps its ellipsis in a hard `\\x1b[0m`, which would strip a\n * row's background for everything after a clipped name. The theme closes its own\n * colours with `\\x1b[39m` and `\\x1b[49m`, so the injected resets are dropped.\n */\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS).replaceAll(HARD_RESET, '');\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction rightAligned(left: string, right: string, width: number): string {\n const rightWidth = visibleWidth(right);\n const leftWidth = Math.max(0, width - rightWidth - 1);\n return fit(left, leftWidth) + ' '.repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);\n}\n\nfunction jobGlyph(status: string): string {\n return status === 'completed' ? '✓' : status === 'failed' ? '✗' : '●';\n}\n\nfunction jobColour(status: string): ThemeColor {\n return status === 'completed' ? 'success' : status === 'failed' ? 'error' : 'accent';\n}\n\nfunction progressLines(item: WorkflowInspectorItem, width: number, theme: Theme): string[] {\n const lines: string[] = [];\n for (const job of item.jobs) {\n const position = job.total ? ` · ${(job.index ?? 0) + 1}/${job.total}` : '';\n lines.push(\n truncateToWidth(\n theme.fg(jobColour(job.status), `${jobGlyph(job.status)} ${job.name}`) + theme.fg('dim', position),\n width,\n ELLIPSIS,\n ),\n );\n // Only the running job's steps: every finished job's steps would push the\n // live one off the pane, which is the one thing worth watching.\n if (job.status !== RUNNING_STATE) continue;\n for (const step of job.steps) {\n const stepColour: ThemeColor =\n step.status === 'failed' ? 'error' : step.status === RUNNING_STATE ? 'muted' : 'dim';\n lines.push(truncateToWidth(theme.fg(stepColour, ` ${jobGlyph(step.status)} ${step.name}`), width, ELLIPSIS));\n }\n }\n return lines;\n}\n\nfunction detailLines(item: WorkflowInspectorItem | undefined, width: number, height: number, theme: Theme): string[] {\n if (!item) return [theme.fg('dim', NO_SELECTION)];\n\n const position = currentWorkflowPosition(item.jobs);\n const elapsed = humanizeDuration(Math.max(0, Date.now() - Date.parse(item.startedAt)));\n const executionState = item.executionState ?? RUNNING_STATE;\n const stateColour: ThemeColor = executionState === PAUSED_STATE ? 'warning' : 'dim';\n const lines = [\n rightAligned(\n `${theme.fg('accent', '●')} ${theme.bold(item.displayName)}`,\n theme.fg(stateColour, executionState),\n width,\n ),\n truncateToWidth(theme.fg('dim', `${item.runKey} · ${item.workspace} · ${elapsed}`), width, ELLIPSIS),\n position\n ? truncateToWidth(\n `${theme.fg('accent', position.job)}${position.step ? theme.fg('muted', ` · ${position.step}`) : ''}`,\n width,\n ELLIPSIS,\n )\n : theme.fg('dim', 'starting'),\n '',\n theme.fg('dim', PROGRESS_HEADING),\n ];\n\n // The output tail claims its rows first: progress is a list that can be paged\n // to by opening the run, while the tail is the only live signal on screen.\n const reservedOutput = item.output.length > 0 ? MIN_OUTPUT_ROWS + 1 : 0;\n const progressRoom = Math.max(0, height - lines.length - reservedOutput);\n lines.push(...progressLines(item, width, theme).slice(0, progressRoom));\n\n if (item.output.length > 0 && height - lines.length >= MIN_OUTPUT_ROWS) {\n lines.push('', theme.fg('dim', OUTPUT_HEADING));\n const outputRoom = Math.max(0, height - lines.length);\n lines.push(...item.output.slice(-outputRoom).map((line) => truncateToWidth(line, width, ELLIPSIS)));\n }\n\n return lines.slice(0, height);\n}\n\nexport class WorkflowInspectorComponent extends DoomOverlay {\n private bodyHeight = 8;\n private completed = false;\n private error: string | undefined;\n private items: WorkflowInspectorItem[] = [];\n private refreshActive = false;\n private selected = 0;\n private selectedKey: string | undefined;\n private readonly timer: ReturnType<typeof setInterval>;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly source: WorkflowInspectorSource,\n private readonly done: (selection: WorkflowInspectorSelection | undefined) => void,\n options: WorkflowInspectorOptions = {},\n ) {\n super(tui, theme);\n this.selectedKey = options.initialKey;\n void this.refresh();\n this.timer = setInterval(() => void this.refresh(), options.refreshMs ?? DEFAULT_REFRESH_MS);\n this.timer.unref?.();\n }\n\n private async refresh(): Promise<void> {\n if (this.refreshActive || this.completed) return;\n this.refreshActive = true;\n try {\n const previousKey = this.items[this.selected]?.key ?? this.selectedKey;\n const items = await this.source.list();\n if (this.completed) return;\n const preserved = previousKey ? items.findIndex((item) => item.key === previousKey) : -1;\n this.items = items;\n this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, items.length - 1));\n this.selectedKey = items[this.selected]?.key;\n const selected = items[this.selected];\n if (selected) selected.output = await this.source.output(selected);\n this.error = undefined;\n if (!this.completed) this.tui.requestRender();\n } catch (cause) {\n this.error = cause instanceof Error ? cause.message : String(cause);\n if (!this.completed) this.tui.requestRender();\n } finally {\n this.refreshActive = false;\n }\n }\n\n private move(delta: number): void {\n if (this.items.length === 0) return;\n this.selected = Math.max(0, Math.min(this.items.length - 1, this.selected + delta));\n this.selectedKey = this.items[this.selected]?.key;\n void this.refreshSelectedOutput();\n this.tui.requestRender();\n }\n\n private async refreshSelectedOutput(): Promise<void> {\n const selected = this.items[this.selected];\n if (!selected || this.completed) return;\n try {\n selected.output = await this.source.output(selected);\n this.error = undefined;\n } catch (cause) {\n this.error = cause instanceof Error ? cause.message : String(cause);\n }\n if (!this.completed && selected.key === this.selectedKey) this.tui.requestRender();\n }\n\n private finish(selection: WorkflowInspectorSelection | undefined): void {\n if (this.completed) return;\n this.completed = true;\n clearInterval(this.timer);\n this.done(selection);\n }\n\n close(): void {\n this.finish(undefined);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.finish(undefined);\n return;\n }\n if (matchesKey(data, 'up') || data === 'k') {\n this.move(-1);\n return;\n }\n if (matchesKey(data, 'down') || data === 'j') {\n this.move(1);\n return;\n }\n if (matchesKey(data, 'pageUp')) {\n this.move(-this.rosterCapacity());\n return;\n }\n if (matchesKey(data, 'pageDown')) {\n this.move(this.rosterCapacity());\n return;\n }\n if (matchesKey(data, 'enter')) {\n const selected = this.items[this.selected];\n if (selected) {\n this.finish({\n ...(selected.runId ? { runId: selected.runId } : {}),\n runKey: selected.runKey,\n startedAt: selected.startedAt,\n workspace: selected.workspace,\n });\n }\n }\n }\n\n /** Runs the roster can draw, which is also what a page key moves by. */\n private rosterCapacity(): number {\n return Math.max(1, Math.floor((this.bodyHeight - ROSTER_CHROME_ROWS) / ROWS_PER_ITEM));\n }\n\n private current(): WorkflowInspectorItem | undefined {\n if (this.items.length === 0) return undefined;\n return this.items[Math.min(this.selected, this.items.length - 1)];\n }\n\n protected getChrome(): DoomOverlayChrome {\n const selected = this.current();\n const summary = selected\n ? `${selected.displayName} · ${selected.executionState ?? RUNNING_STATE}`\n : 'no active workflows';\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${this.items.length} running · ${summary}`,\n footer: FOOTER,\n footerHints: FOOTER_HINTS,\n footerRight:\n this.items.length > 0 ? `${Math.min(this.selected, this.items.length - 1) + 1}/${this.items.length}` : 'empty',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n this.bodyHeight = height;\n const detailFor = (paneWidth: number): string[] =>\n this.error\n ? [this.theme.fg('warning', `Unable to refresh workflows: ${this.error}`)]\n : detailLines(this.current(), paneWidth, height, this.theme);\n\n // Too narrow for two useful columns: the detail takes the whole body rather\n // than shredding both panes into stubs.\n if (width < MIN_TWO_PANE_WIDTH) return detailFor(width).slice(0, height);\n\n const leftWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * LIST_PANE_RATIO));\n const rightWidth = Math.max(1, width - leftWidth - 1);\n const leftContent = Math.max(1, leftWidth - PANE_GUTTER);\n const rightContent = Math.max(1, rightWidth - PANE_GUTTER);\n\n const left = this.rosterLines(leftContent);\n const right = detailFor(rightContent);\n const divider = this.theme.fg('borderMuted', '│');\n return Array.from({ length: height }, (_, index) => {\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fit(right[index] ?? '', rightContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n }\n\n private rosterLines(width: number): string[] {\n const lines = [this.theme.bold(ROSTER_HEADING), ''];\n if (this.items.length === 0) {\n lines.push(this.theme.fg('dim', EMPTY_ROSTER));\n return lines;\n }\n\n // Two rows per run: at a third of the width a single row left the name with\n // a handful of columns once the stage took its share.\n const budget = this.rosterCapacity();\n const active = Math.min(this.selected, this.items.length - 1);\n const start = Math.max(0, Math.min(active - budget + 1, Math.max(0, this.items.length - budget)));\n for (const [offset, item] of this.items.slice(start, start + budget).entries()) {\n const current = start + offset === active;\n const paused = item.executionState === PAUSED_STATE;\n const marker = current ? this.theme.fg('accent', SELECTION_MARKER) : ' ';\n const glyph = this.theme.fg(paused ? 'warning' : 'accent', '●');\n const name = current ? this.theme.bold(item.displayName) : item.displayName;\n const stage = paused ? PAUSED_STATE : (currentWorkflowPosition(item.jobs)?.job ?? 'starting');\n const heading = `${marker} ${glyph} ${name}`;\n const meta = `${' '.repeat(META_INDENT)}${this.theme.fg(paused ? 'warning' : 'dim', stage)}`;\n const background: ThemeBg = current ? 'selectedBg' : 'userMessageBg';\n for (const row of [heading, meta]) lines.push(this.theme.bg(background, fit(row, width)));\n }\n return lines;\n }\n\n /** Overridden: a repaint request is also the cue to re-read the registry. */\n invalidate(): void {\n void this.refresh();\n }\n\n dispose(): void {\n if (this.completed) return;\n this.completed = true;\n clearInterval(this.timer);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import{DOOM_FULLSCREEN_UI_OPTIONS as e,DOOM_OVERLAY_ACCENT as t,DoomOverlay as n}from"./doomOverlay.mjs";import{humanizeDuration as r}from"./workflowStatusRow.mjs";import{currentWorkflowPosition as i}from"@agimon-ai/workflow-mcp";import{matchesKey as a,truncateToWidth as o,visibleWidth as s}from"@earendil-works/pi-tui";e.overlayOptions;const c=`running`,l=`paused`,u=`↑↓`,d=`${u} select · PgUp/PgDn page · enter open · esc close`,f=[[u,`select`],[`PgUp/PgDn`,`page`],[`enter`,`open`],[`esc`,`close`]];function p(e,t){let n=o(e,Math.max(0,t),`…`).replaceAll(`\x1B[0m`,``);return n+` `.repeat(Math.max(0,t-s(n)))}function m(e,t,n){let r=s(t),i=Math.max(0,n-r-1);return p(e,i)+` `.repeat(Math.max(1,n-i-r))+p(t,r)}function h(e){return e===`completed`?`✓`:e===`failed`?`✗`:`●`}function g(e){return e===`completed`?`success`:e===`failed`?`error`:`accent`}function _(e,t,n){let r=[];for(let i of e.jobs){let e=i.total?` · ${(i.index??0)+1}/${i.total}`:``;if(r.push(o(n.fg(g(i.status),`${h(i.status)} ${i.name}`)+n.fg(`dim`,e),t,`…`)),i.status===c)for(let e of i.steps){let i=e.status===`failed`?`error`:e.status===c?`muted`:`dim`;r.push(o(n.fg(i,` ${h(e.status)} ${e.name}`),t,`…`))}}return r}function v(e,t,n,a){if(!e)return[a.fg(`dim`,`Select a workflow to inspect its jobs and output.`)];let s=i(e.jobs),u=r(Math.max(0,Date.now()-Date.parse(e.startedAt))),d=e.executionState??c,f=d===l?`warning`:`dim`,p=[m(`${a.fg(`accent`,`●`)} ${a.bold(e.displayName)}`,a.fg(f,d),t),o(a.fg(`dim`,`${e.runKey} · ${e.workspace} · ${u}`),t,`…`),s?o(`${a.fg(`accent`,s.job)}${s.step?a.fg(`muted`,` · ${s.step}`):``}`,t,`…`):a.fg(`dim`,`starting`),``,a.fg(`dim`,`PROGRESS`)],h=e.output.length>0?3:0,g=Math.max(0,n-p.length-h);if(p.push(..._(e,t,a).slice(0,g)),e.output.length>0&&n-p.length>=2){p.push(``,a.fg(`dim`,`OUTPUT · newest last`));let r=Math.max(0,n-p.length);p.push(...e.output.slice(-r).map(e=>o(e,t,`…`)))}return p.slice(0,n)}var y=class extends n{source;done;bodyHeight=8;completed=!1;error;items=[];refreshActive=!1;selected=0;selectedKey;timer;constructor(e,t,n,r,i={}){super(e,t),this.source=n,this.done=r,this.selectedKey=i.initialKey,this.refresh(),this.timer=setInterval(()=>void this.refresh(),i.refreshMs??750),this.timer.unref?.()}async refresh(){if(!(this.refreshActive||this.completed)){this.refreshActive=!0;try{let e=this.items[this.selected]?.key??this.selectedKey,t=await this.source.list();if(this.completed)return;let n=e?t.findIndex(t=>t.key===e):-1;this.items=t,this.selected=n>=0?n:Math.min(this.selected,Math.max(0,t.length-1)),this.selectedKey=t[this.selected]?.key;let r=t[this.selected];r&&(r.output=await this.source.output(r)),this.error=void 0,this.completed||this.tui.requestRender()}catch(e){this.error=e instanceof Error?e.message:String(e),this.completed||this.tui.requestRender()}finally{this.refreshActive=!1}}}move(e){this.items.length!==0&&(this.selected=Math.max(0,Math.min(this.items.length-1,this.selected+e)),this.selectedKey=this.items[this.selected]?.key,this.refreshSelectedOutput(),this.tui.requestRender())}async refreshSelectedOutput(){let e=this.items[this.selected];if(!(!e||this.completed)){try{e.output=await this.source.output(e),this.error=void 0}catch(e){this.error=e instanceof Error?e.message:String(e)}!this.completed&&e.key===this.selectedKey&&this.tui.requestRender()}}finish(e){this.completed||(this.completed=!0,clearInterval(this.timer),this.done(e))}close(){this.finish(void 0)}handleInput(e){if(a(e,`escape`)||a(e,`ctrl+c`)){this.finish(void 0);return}if(a(e,`up`)||e===`k`){this.move(-1);return}if(a(e,`down`)||e===`j`){this.move(1);return}if(a(e,`pageUp`)){this.move(-this.rosterCapacity());return}if(a(e,`pageDown`)){this.move(this.rosterCapacity());return}if(a(e,`enter`)){let e=this.items[this.selected];e&&this.finish({...e.runId?{runId:e.runId}:{},runKey:e.runKey,startedAt:e.startedAt,workspace:e.workspace})}}rosterCapacity(){return Math.max(1,Math.floor((this.bodyHeight-2)/2))}current(){if(this.items.length!==0)return this.items[Math.min(this.selected,this.items.length-1)]}getChrome(){let e=this.current(),n=e?`${e.displayName} · ${e.executionState??c}`:`no active workflows`;return{title:`WORKFLOW SPACE`,accent:t,breadcrumb:`SPC › w / workflows › l / manage`,headerRight:`${this.items.length} running · ${n}`,footer:d,footerHints:f,footerRight:this.items.length>0?`${Math.min(this.selected,this.items.length-1)+1}/${this.items.length}`:`empty`}}renderBody(e,t){this.bodyHeight=t;let n=e=>this.error?[this.theme.fg(`warning`,`Unable to refresh workflows: ${this.error}`)]:v(this.current(),e,t,this.theme);if(e<48)return n(e).slice(0,t);let r=Math.max(18,Math.floor((e-1)*.3333333333333333)),i=Math.max(1,e-r-1),a=Math.max(1,r-1),s=Math.max(1,i-1),c=this.rosterLines(a),l=n(s),u=this.theme.fg(`borderMuted`,`│`);return Array.from({length:t},(t,n)=>{let r=`${p(c[n]??``,a)} ${u} ${p(l[n]??``,s)}`;return o(r,e,`…`)})}rosterLines(e){let t=[this.theme.bold(`RUNNING · this session`),``];if(this.items.length===0)return t.push(this.theme.fg(`dim`,`No active workflows`)),t;let n=this.rosterCapacity(),r=Math.min(this.selected,this.items.length-1),a=Math.max(0,Math.min(r-n+1,Math.max(0,this.items.length-n)));for(let[o,s]of this.items.slice(a,a+n).entries()){let n=a+o===r,c=s.executionState===l,u=n?this.theme.fg(`accent`,`›`):` `,d=this.theme.fg(c?`warning`:`accent`,`●`),f=n?this.theme.bold(s.displayName):s.displayName,m=c?l:i(s.jobs)?.job??`starting`,h=`${u} ${d} ${f}`,g=`${` `.repeat(4)}${this.theme.fg(c?`warning`:`dim`,m)}`,_=n?`selectedBg`:`userMessageBg`;for(let n of[h,g])t.push(this.theme.bg(_,p(n,e)))}return t}invalidate(){this.refresh()}dispose(){this.completed||(this.completed=!0,clearInterval(this.timer))}};export{y as WorkflowInspectorComponent};
2
+ //# sourceMappingURL=workflowInspector.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowInspector.mjs","names":[],"sources":["../../../src/tui/workflow/workflowInspector.ts"],"sourcesContent":["/**\n * Workflow Space: the `SPC w l` overlay.\n *\n * A doom overlay rather than a hand-rolled frame: the roster owns the left\n * third, the run under the cursor owns the right two thirds, and the chrome is\n * the same frame, breadcrumb and key legend every other doom surface draws.\n *\n * Rendering is pure -- `renderBody(width, height)` reads the last polled\n * snapshot and returns lines with no side effects -- so the surface can be\n * asserted as text without a live terminal.\n */\n\nimport { currentWorkflowPosition, type WorkflowProgressJob } from '@agimon-ai/workflow-mcp';\nimport type { Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay.ts';\nimport { humanizeDuration } from './workflowStatusRow';\n\nexport const WORKFLOW_INSPECTOR_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nexport interface WorkflowInspectorSelection {\n runId?: string;\n runKey: string;\n startedAt: string;\n workspace: string;\n}\n\nexport interface WorkflowInspectorItem extends WorkflowInspectorSelection {\n displayName: string;\n executionState?: string;\n jobs: WorkflowProgressJob[];\n key: string;\n output: string[];\n}\n\nexport interface WorkflowInspectorSource {\n list(): Promise<WorkflowInspectorItem[]>;\n output(item: WorkflowInspectorItem): Promise<string[]>;\n}\n\nexport interface WorkflowInspectorOptions {\n initialKey?: string;\n refreshMs?: number;\n}\n\nconst TITLE = 'WORKFLOW SPACE';\nconst BREADCRUMB = 'SPC › w / workflows › l / manage';\nconst ROSTER_HEADING = 'RUNNING · this session';\nconst EMPTY_ROSTER = 'No active workflows';\nconst PROGRESS_HEADING = 'PROGRESS';\nconst OUTPUT_HEADING = 'OUTPUT · newest last';\nconst NO_SELECTION = 'Select a workflow to inspect its jobs and output.';\nconst RUNNING_STATE = 'running';\nconst PAUSED_STATE = 'paused';\n/** Mirrors DoomPi's list-navigation legend without adding a runtime doompi-ui dependency. */\nconst LIST_NAVIGATION_KEYS = '↑↓';\nconst FOOTER = `${LIST_NAVIGATION_KEYS} select · PgUp/PgDn page · enter open · esc close`;\nconst FOOTER_HINTS: readonly (readonly [string, string])[] = [\n [LIST_NAVIGATION_KEYS, 'select'],\n ['PgUp/PgDn', 'page'],\n ['enter', 'open'],\n ['esc', 'close'],\n];\n\nconst DEFAULT_REFRESH_MS = 750;\nconst SELECTION_MARKER = '›';\nconst ELLIPSIS = '…';\n/** pi-tui brackets a truncation ellipsis with this; see `fit`. */\nconst HARD_RESET = '\\x1b[0m';\n/** A third for the roster, two thirds for the run being read. */\nconst LIST_PANE_RATIO = 1 / 3;\n/** One blank column each side of the divider so neither pane touches it. */\nconst PANE_GUTTER = 1;\nconst MIN_PANE_WIDTH = 18;\n/** Below this the roster cannot hold a readable name, so the detail takes the body. */\nconst MIN_TWO_PANE_WIDTH = 48;\n/** Each run occupies a name row and a state row beneath it. */\nconst ROWS_PER_ITEM = 2;\n/** Hanging indent for the state row: clears the marker and the glyph. */\nconst META_INDENT = 4;\n/** Rows the roster heading and its spacer take. */\nconst ROSTER_CHROME_ROWS = 2;\n/** The output tail keeps at least this many rows, or it is not worth a divider. */\nconst MIN_OUTPUT_ROWS = 2;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\n/**\n * Truncates and pads to an exact column count.\n *\n * `truncateToWidth` wraps its ellipsis in a hard `\\x1b[0m`, which would strip a\n * row's background for everything after a clipped name. The theme closes its own\n * colours with `\\x1b[39m` and `\\x1b[49m`, so the injected resets are dropped.\n */\nfunction fit(text: string, width: number): string {\n const clipped = truncateToWidth(text, Math.max(0, width), ELLIPSIS).replaceAll(HARD_RESET, '');\n return clipped + ' '.repeat(Math.max(0, width - visibleWidth(clipped)));\n}\n\nfunction rightAligned(left: string, right: string, width: number): string {\n const rightWidth = visibleWidth(right);\n const leftWidth = Math.max(0, width - rightWidth - 1);\n return fit(left, leftWidth) + ' '.repeat(Math.max(1, width - leftWidth - rightWidth)) + fit(right, rightWidth);\n}\n\nfunction jobGlyph(status: string): string {\n return status === 'completed' ? '✓' : status === 'failed' ? '✗' : '●';\n}\n\nfunction jobColour(status: string): ThemeColor {\n return status === 'completed' ? 'success' : status === 'failed' ? 'error' : 'accent';\n}\n\nfunction progressLines(item: WorkflowInspectorItem, width: number, theme: Theme): string[] {\n const lines: string[] = [];\n for (const job of item.jobs) {\n const position = job.total ? ` · ${(job.index ?? 0) + 1}/${job.total}` : '';\n lines.push(\n truncateToWidth(\n theme.fg(jobColour(job.status), `${jobGlyph(job.status)} ${job.name}`) + theme.fg('dim', position),\n width,\n ELLIPSIS,\n ),\n );\n // Only the running job's steps: every finished job's steps would push the\n // live one off the pane, which is the one thing worth watching.\n if (job.status !== RUNNING_STATE) continue;\n for (const step of job.steps) {\n const stepColour: ThemeColor =\n step.status === 'failed' ? 'error' : step.status === RUNNING_STATE ? 'muted' : 'dim';\n lines.push(truncateToWidth(theme.fg(stepColour, ` ${jobGlyph(step.status)} ${step.name}`), width, ELLIPSIS));\n }\n }\n return lines;\n}\n\nfunction detailLines(item: WorkflowInspectorItem | undefined, width: number, height: number, theme: Theme): string[] {\n if (!item) return [theme.fg('dim', NO_SELECTION)];\n\n const position = currentWorkflowPosition(item.jobs);\n const elapsed = humanizeDuration(Math.max(0, Date.now() - Date.parse(item.startedAt)));\n const executionState = item.executionState ?? RUNNING_STATE;\n const stateColour: ThemeColor = executionState === PAUSED_STATE ? 'warning' : 'dim';\n const lines = [\n rightAligned(\n `${theme.fg('accent', '●')} ${theme.bold(item.displayName)}`,\n theme.fg(stateColour, executionState),\n width,\n ),\n truncateToWidth(theme.fg('dim', `${item.runKey} · ${item.workspace} · ${elapsed}`), width, ELLIPSIS),\n position\n ? truncateToWidth(\n `${theme.fg('accent', position.job)}${position.step ? theme.fg('muted', ` · ${position.step}`) : ''}`,\n width,\n ELLIPSIS,\n )\n : theme.fg('dim', 'starting'),\n '',\n theme.fg('dim', PROGRESS_HEADING),\n ];\n\n // The output tail claims its rows first: progress is a list that can be paged\n // to by opening the run, while the tail is the only live signal on screen.\n const reservedOutput = item.output.length > 0 ? MIN_OUTPUT_ROWS + 1 : 0;\n const progressRoom = Math.max(0, height - lines.length - reservedOutput);\n lines.push(...progressLines(item, width, theme).slice(0, progressRoom));\n\n if (item.output.length > 0 && height - lines.length >= MIN_OUTPUT_ROWS) {\n lines.push('', theme.fg('dim', OUTPUT_HEADING));\n const outputRoom = Math.max(0, height - lines.length);\n lines.push(...item.output.slice(-outputRoom).map((line) => truncateToWidth(line, width, ELLIPSIS)));\n }\n\n return lines.slice(0, height);\n}\n\nexport class WorkflowInspectorComponent extends DoomOverlay {\n private bodyHeight = 8;\n private completed = false;\n private error: string | undefined;\n private items: WorkflowInspectorItem[] = [];\n private refreshActive = false;\n private selected = 0;\n private selectedKey: string | undefined;\n private readonly timer: ReturnType<typeof setInterval>;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly source: WorkflowInspectorSource,\n private readonly done: (selection: WorkflowInspectorSelection | undefined) => void,\n options: WorkflowInspectorOptions = {},\n ) {\n super(tui, theme);\n this.selectedKey = options.initialKey;\n void this.refresh();\n this.timer = setInterval(() => void this.refresh(), options.refreshMs ?? DEFAULT_REFRESH_MS);\n this.timer.unref?.();\n }\n\n private async refresh(): Promise<void> {\n if (this.refreshActive || this.completed) return;\n this.refreshActive = true;\n try {\n const previousKey = this.items[this.selected]?.key ?? this.selectedKey;\n const items = await this.source.list();\n if (this.completed) return;\n const preserved = previousKey ? items.findIndex((item) => item.key === previousKey) : -1;\n this.items = items;\n this.selected = preserved >= 0 ? preserved : Math.min(this.selected, Math.max(0, items.length - 1));\n this.selectedKey = items[this.selected]?.key;\n const selected = items[this.selected];\n if (selected) selected.output = await this.source.output(selected);\n this.error = undefined;\n if (!this.completed) this.tui.requestRender();\n } catch (cause) {\n this.error = cause instanceof Error ? cause.message : String(cause);\n if (!this.completed) this.tui.requestRender();\n } finally {\n this.refreshActive = false;\n }\n }\n\n private move(delta: number): void {\n if (this.items.length === 0) return;\n this.selected = Math.max(0, Math.min(this.items.length - 1, this.selected + delta));\n this.selectedKey = this.items[this.selected]?.key;\n void this.refreshSelectedOutput();\n this.tui.requestRender();\n }\n\n private async refreshSelectedOutput(): Promise<void> {\n const selected = this.items[this.selected];\n if (!selected || this.completed) return;\n try {\n selected.output = await this.source.output(selected);\n this.error = undefined;\n } catch (cause) {\n this.error = cause instanceof Error ? cause.message : String(cause);\n }\n if (!this.completed && selected.key === this.selectedKey) this.tui.requestRender();\n }\n\n private finish(selection: WorkflowInspectorSelection | undefined): void {\n if (this.completed) return;\n this.completed = true;\n clearInterval(this.timer);\n this.done(selection);\n }\n\n close(): void {\n this.finish(undefined);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.finish(undefined);\n return;\n }\n if (matchesKey(data, 'up') || data === 'k') {\n this.move(-1);\n return;\n }\n if (matchesKey(data, 'down') || data === 'j') {\n this.move(1);\n return;\n }\n if (matchesKey(data, 'pageUp')) {\n this.move(-this.rosterCapacity());\n return;\n }\n if (matchesKey(data, 'pageDown')) {\n this.move(this.rosterCapacity());\n return;\n }\n if (matchesKey(data, 'enter')) {\n const selected = this.items[this.selected];\n if (selected) {\n this.finish({\n ...(selected.runId ? { runId: selected.runId } : {}),\n runKey: selected.runKey,\n startedAt: selected.startedAt,\n workspace: selected.workspace,\n });\n }\n }\n }\n\n /** Runs the roster can draw, which is also what a page key moves by. */\n private rosterCapacity(): number {\n return Math.max(1, Math.floor((this.bodyHeight - ROSTER_CHROME_ROWS) / ROWS_PER_ITEM));\n }\n\n private current(): WorkflowInspectorItem | undefined {\n if (this.items.length === 0) return undefined;\n return this.items[Math.min(this.selected, this.items.length - 1)];\n }\n\n protected getChrome(): DoomOverlayChrome {\n const selected = this.current();\n const summary = selected\n ? `${selected.displayName} · ${selected.executionState ?? RUNNING_STATE}`\n : 'no active workflows';\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${this.items.length} running · ${summary}`,\n footer: FOOTER,\n footerHints: FOOTER_HINTS,\n footerRight:\n this.items.length > 0 ? `${Math.min(this.selected, this.items.length - 1) + 1}/${this.items.length}` : 'empty',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n this.bodyHeight = height;\n const detailFor = (paneWidth: number): string[] =>\n this.error\n ? [this.theme.fg('warning', `Unable to refresh workflows: ${this.error}`)]\n : detailLines(this.current(), paneWidth, height, this.theme);\n\n // Too narrow for two useful columns: the detail takes the whole body rather\n // than shredding both panes into stubs.\n if (width < MIN_TWO_PANE_WIDTH) return detailFor(width).slice(0, height);\n\n const leftWidth = Math.max(MIN_PANE_WIDTH, Math.floor((width - 1) * LIST_PANE_RATIO));\n const rightWidth = Math.max(1, width - leftWidth - 1);\n const leftContent = Math.max(1, leftWidth - PANE_GUTTER);\n const rightContent = Math.max(1, rightWidth - PANE_GUTTER);\n\n const left = this.rosterLines(leftContent);\n const right = detailFor(rightContent);\n const divider = this.theme.fg('borderMuted', '│');\n return Array.from({ length: height }, (_, index) => {\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fit(right[index] ?? '', rightContent)}`;\n return truncateToWidth(row, width, ELLIPSIS);\n });\n }\n\n private rosterLines(width: number): string[] {\n const lines = [this.theme.bold(ROSTER_HEADING), ''];\n if (this.items.length === 0) {\n lines.push(this.theme.fg('dim', EMPTY_ROSTER));\n return lines;\n }\n\n // Two rows per run: at a third of the width a single row left the name with\n // a handful of columns once the stage took its share.\n const budget = this.rosterCapacity();\n const active = Math.min(this.selected, this.items.length - 1);\n const start = Math.max(0, Math.min(active - budget + 1, Math.max(0, this.items.length - budget)));\n for (const [offset, item] of this.items.slice(start, start + budget).entries()) {\n const current = start + offset === active;\n const paused = item.executionState === PAUSED_STATE;\n const marker = current ? this.theme.fg('accent', SELECTION_MARKER) : ' ';\n const glyph = this.theme.fg(paused ? 'warning' : 'accent', '●');\n const name = current ? this.theme.bold(item.displayName) : item.displayName;\n const stage = paused ? PAUSED_STATE : (currentWorkflowPosition(item.jobs)?.job ?? 'starting');\n const heading = `${marker} ${glyph} ${name}`;\n const meta = `${' '.repeat(META_INDENT)}${this.theme.fg(paused ? 'warning' : 'dim', stage)}`;\n const background: ThemeBg = current ? 'selectedBg' : 'userMessageBg';\n for (const row of [heading, meta]) lines.push(this.theme.bg(background, fit(row, width)));\n }\n return lines;\n }\n\n /** Overridden: a repaint request is also the cue to re-read the registry. */\n invalidate(): void {\n void this.refresh();\n }\n\n dispose(): void {\n if (this.completed) return;\n this.completed = true;\n clearInterval(this.timer);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"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":"wCA0DA,IAAa,EAAb,KAAkC,CAGH,SAF7B,aAEA,YAAY,EAAiB,IAA4C,CAA5C,KAAA,SAAA,CAA6C,CAQ1E,QAAQ,EAAc,EAAc,KAAK,IAAI,EAAiB,CA2B5D,OAtBI,IAAS,YACX,KAAK,aAAe,IAAA,GACb,UAKT,EAAIA,EAAAA,aAAAA,CAAa,CAAI,EAAU,WAE3B,EAACC,EAAAA,WAAAA,CAAW,EAAM,QAAQ,EAO1B,KAAK,eAAiB,IAAA,IAAa,EAAM,KAAK,cAAgB,KAAK,UACrE,KAAK,aAAe,IAAA,GACb,UAGT,KAAK,aAAe,EACb,YAVL,KAAK,aAAe,IAAA,GACb,UAUX,CACF,EAQa,EAAb,KAAkC,CAMb,KACA,gBANnB,QAAkB,GAClB,MACA,SAAmB,GAEnB,YACE,EACA,EAA2C,GAC3C,CAFiB,KAAA,KAAA,EACA,KAAA,gBAAA,CAChB,CAEH,MAAM,EAAoB,CACpB,KAAK,UAAY,EAAK,SAAW,IACrC,KAAK,SAAW,EACZ,MAAK,QACT,KAAK,MAAQ,eAAiB,KAAK,KAAK,MAAM,EAAG,KAAK,eAAe,EACrE,KAAK,MAAM,QAAQ,GACrB,CAGA,MAAM,OAAuB,CACvB,KAAK,OAAO,aAAa,KAAK,KAAK,EACvC,KAAK,MAAQ,IAAA,GACb,IAAM,EAAO,KAAK,QAClB,QAAK,QAAU,GACX,GAAC,GAAQ,KAAK,UAClB,GAAI,CACF,MAAM,KAAK,KAAK,CAAI,CACtB,MAAQ,CAER,CACF,CAGA,SAAgB,CACd,KAAK,SAAW,GACZ,KAAK,OAAO,aAAa,KAAK,KAAK,EACvC,KAAK,MAAQ,IAAA,GACb,KAAK,QAAU,EACjB,CAGA,YAAsB,CACpB,OAAO,KAAK,QAAQ,OAAS,CAC/B,CACF"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowOverlay.mjs","names":[],"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":"sEA0DA,IAAa,EAAb,KAAkC,CAGH,SAF7B,aAEA,YAAY,EAAiB,IAA4C,CAA5C,KAAA,SAAA,CAA6C,CAQ1E,QAAQ,EAAc,EAAc,KAAK,IAAI,EAAiB,CA2B5D,OAtBI,IAAS,YACX,KAAK,aAAe,IAAA,GACb,SAKL,EAAa,CAAI,EAAU,UAE1B,EAAW,EAAM,QAAQ,EAO1B,KAAK,eAAiB,IAAA,IAAa,EAAM,KAAK,cAAgB,KAAK,UACrE,KAAK,aAAe,IAAA,GACb,UAGT,KAAK,aAAe,EACb,YAVL,KAAK,aAAe,IAAA,GACb,UAUX,CACF,EAQa,EAAb,KAAkC,CAMb,KACA,gBANnB,QAAkB,GAClB,MACA,SAAmB,GAEnB,YACE,EACA,EAA2C,GAC3C,CAFiB,KAAA,KAAA,EACA,KAAA,gBAAA,CAChB,CAEH,MAAM,EAAoB,CACpB,KAAK,UAAY,EAAK,SAAW,IACrC,KAAK,SAAW,EACZ,MAAK,QACT,KAAK,MAAQ,eAAiB,KAAK,KAAK,MAAM,EAAG,KAAK,eAAe,EACrE,KAAK,MAAM,QAAQ,GACrB,CAGA,MAAM,OAAuB,CACvB,KAAK,OAAO,aAAa,KAAK,KAAK,EACvC,KAAK,MAAQ,IAAA,GACb,IAAM,EAAO,KAAK,QAClB,QAAK,QAAU,GACX,GAAC,GAAQ,KAAK,UAClB,GAAI,CACF,MAAM,KAAK,KAAK,CAAI,CACtB,MAAQ,CAER,CACF,CAGA,SAAgB,CACd,KAAK,SAAW,GACZ,KAAK,OAAO,aAAa,KAAK,KAAK,EACvC,KAAK,MAAQ,IAAA,GACb,KAAK,QAAU,EACjB,CAGA,YAAsB,CACpB,OAAO,KAAK,QAAQ,OAAS,CAC/B,CACF"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowPicker.cjs","names":["DOOM_FULLSCREEN_UI_OPTIONS","matchesQuery","DoomOverlay","matchesKey","isControlInput","DOOM_OVERLAY_ACCENT","fit","pathTail"],"sources":["../../../src/tui/workflow/workflowPicker.ts"],"sourcesContent":["/**\n * Scrollable, filterable list picker for every long list this extension shows.\n *\n * Replaces a bare `ui.select`, which wrapped each `name · detail` pair across\n * two or three terminal rows and left no way to reach a row other than scrolling\n * past all of them. This is a doom overlay instead: one row per item, a windowed\n * list that scrolls with the selection, and a filter field that narrows as you\n * type.\n *\n * Generic over the value it hands back, so `SPC w w` picks a catalog entry and\n * `SPC w r` picks a failed run through the same surface.\n *\n * Rendering is pure, so the surface can be asserted as text without a terminal.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey } from '@earendil-works/pi-tui';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay.ts';\nimport { CURSOR_BLOCK, fit, isControlInput, matchesQuery, pathTail, SELECTION_MARKER } from './overlayText';\n\nexport const WORKFLOW_PICKER_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst FILTER_LABEL = 'FILTER';\nconst EMPTY_NAME = '(unnamed)';\nconst KEY_CLEAR = '\\x15';\n/** Rows the filter field and its trailing spacer take from the body. */\nconst FILTER_ROWS = 3;\n/** Columns the marker and its trailing space take from a row. */\nconst MARKER_COLUMNS = 2;\n/** Blank columns between the name column and the detail column. */\nconst COLUMN_GAP = 2;\nconst MIN_NAME_WIDTH = 16;\nconst NAME_RATIO = 0.45;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\nconst DIM: ThemeColor = 'dim';\nconst ACCENT: ThemeColor = 'accent';\nconst SELECTED_BG: ThemeBg = 'selectedBg';\nconst ROW_BG: ThemeBg = 'userMessageBg';\n\nexport interface PickerRow<T> {\n /** Stable identity, used only to keep rows distinct. */\n key: string;\n /** Left column. */\n name: string;\n /** Right column, trimmed from the front so its tail survives. */\n detail: string;\n /** Extra text the filter should match but the row does not show. */\n search?: string;\n value: T;\n}\n\nexport interface WorkflowPickerConfig {\n title: string;\n breadcrumb: string;\n /** Plural noun for the header count, e.g. `workflows`. */\n unit: string;\n /** Verb on the enter key cap, e.g. `launch`. */\n action: string;\n filterPlaceholder: string;\n /** Shown when the source list itself is empty, as opposed to over-filtered. */\n emptyMessage: string;\n}\n\nexport function filterPickerRows<T>(rows: readonly PickerRow<T>[], query: string): PickerRow<T>[] {\n if (!query.trim()) return [...rows];\n return rows.filter((row) => matchesQuery([row.name, row.detail, row.search ?? ''].join(' '), query));\n}\n\nexport class WorkflowPickerComponent<T> extends DoomOverlay {\n private query = '';\n private selected = 0;\n private bodyHeight = 1;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly config: WorkflowPickerConfig,\n private readonly rows: readonly PickerRow<T>[],\n private readonly done: (value: T | undefined) => void,\n ) {\n super(tui, theme);\n }\n\n private matches(): PickerRow<T>[] {\n return filterPickerRows(this.rows, this.query);\n }\n\n /** Rows the list can draw, once the filter field has taken its share. */\n private capacity(): number {\n return Math.max(1, this.bodyHeight - FILTER_ROWS);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n const matches = this.matches();\n if (matchesKey(data, 'enter')) {\n const row = matches[Math.min(this.selected, matches.length - 1)];\n if (row) this.done(row.value);\n return;\n }\n\n if (matchesKey(data, 'up') || matchesKey(data, 'ctrl+p')) this.move(-1, matches.length);\n else if (matchesKey(data, 'down') || matchesKey(data, 'ctrl+n')) this.move(1, matches.length);\n else if (matchesKey(data, 'pageUp')) this.move(-this.capacity(), matches.length);\n else if (matchesKey(data, 'pageDown')) this.move(this.capacity(), matches.length);\n else if (matchesKey(data, 'backspace')) this.setQuery(this.query.slice(0, -1));\n else if (data === KEY_CLEAR) this.setQuery('');\n else if (!isControlInput(data)) this.setQuery(this.query + data);\n else return;\n\n this.tui.requestRender();\n }\n\n /** Editing the filter resets the cursor: the previous index named another row. */\n private setQuery(query: string): void {\n if (query === this.query) return;\n this.query = query;\n this.selected = 0;\n }\n\n private move(delta: number, count: number): void {\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n }\n\n protected getChrome(): DoomOverlayChrome {\n const matches = this.matches();\n const scope = this.query ? `${matches.length} of ${this.rows.length}` : `${this.rows.length}`;\n return {\n title: this.config.title,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.config.breadcrumb,\n headerRight: `${scope} ${this.config.unit}`,\n footer: `↑↓ select · enter ${this.config.action} · type to filter · ctrl+u clear · esc cancel`,\n footerHints: [\n ['↑↓', 'select'],\n ['enter', this.config.action],\n ['type', 'filter'],\n ['ctrl+u', 'clear'],\n ['esc', 'cancel'],\n ],\n footerRight:\n matches.length > 0 ? `${Math.min(this.selected, matches.length - 1) + 1}/${matches.length}` : 'no match',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n this.bodyHeight = height;\n return [...this.filterField(width), ...this.listLines(width)].slice(0, height);\n }\n\n private filterField(width: number): string[] {\n const value = this.query\n ? `${this.query}${CURSOR_BLOCK}`\n : `${CURSOR_BLOCK}${this.theme.fg(DIM, this.config.filterPlaceholder)}`;\n return [this.theme.fg(DIM, FILTER_LABEL), this.theme.bg(SELECTED_BG, fit(` ${value}`, width)), ''];\n }\n\n private listLines(width: number): string[] {\n if (this.rows.length === 0) return [this.theme.fg(DIM, this.config.emptyMessage)];\n const matches = this.matches();\n if (matches.length === 0) return [this.theme.fg(DIM, `No ${this.config.unit} match \"${this.query}\".`)];\n\n const capacity = this.capacity();\n const active = Math.min(this.selected, matches.length - 1);\n // The window follows the cursor rather than the other way round, so a filter\n // that shrinks the list can never strand the selection off-screen.\n const start = Math.max(0, Math.min(active - capacity + 1, Math.max(0, matches.length - capacity)));\n\n const nameWidth = Math.max(MIN_NAME_WIDTH, Math.floor((width - MARKER_COLUMNS - COLUMN_GAP) * NAME_RATIO));\n const detailWidth = Math.max(0, width - MARKER_COLUMNS - COLUMN_GAP - nameWidth);\n return matches.slice(start, start + capacity).map((row, offset) => {\n const current = start + offset === active;\n const marker = current ? this.theme.fg(ACCENT, SELECTION_MARKER) : ' ';\n const name = row.name || EMPTY_NAME;\n const label = current ? this.theme.bold(fit(name, nameWidth)) : fit(name, nameWidth);\n const detail = this.theme.fg(DIM, fit(pathTail(row.detail, detailWidth), detailWidth));\n const background: ThemeBg = current ? SELECTED_BG : ROW_BG;\n return this.theme.bg(background, fit(`${marker} ${label}${' '.repeat(COLUMN_GAP)}${detail}`, width));\n });\n }\n}\n\nexport async function openWorkflowPickerOverlay<T>(\n ctx: ExtensionContext,\n config: WorkflowPickerConfig,\n rows: readonly PickerRow<T>[],\n): Promise<T | undefined> {\n return ctx.ui.custom<T | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowPickerComponent(tui, theme, config, rows, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"4GA0B+CA,EAAAA,2BAA2B,eAE1E,MAkBM,EAAuB,aA2B7B,SAAgB,EAAoB,EAA+B,EAA+B,CAEhG,OADK,EAAM,KAAK,EACT,EAAK,OAAQ,GAAQC,EAAAA,aAAa,CAAC,EAAI,KAAM,EAAI,OAAQ,EAAI,QAAU,EAAE,CAAC,CAAC,KAAK,GAAG,EAAG,CAAK,CAAC,EADzE,CAAC,GAAG,CAAI,CAEpC,CAEA,IAAa,EAAb,cAAgDC,EAAAA,WAAY,CAQvC,OACA,KACA,KATnB,MAAgB,GAChB,SAAmB,EACnB,WAAqB,EAErB,YACE,EACA,EACA,EACA,EACA,EACA,CACA,MAAM,EAAK,CAAK,EAJC,KAAA,OAAA,EACA,KAAA,KAAA,EACA,KAAA,KAAA,CAGnB,CAEA,SAAkC,CAChC,OAAO,EAAiB,KAAK,KAAM,KAAK,KAAK,CAC/C,CAGA,UAA2B,CACzB,OAAO,KAAK,IAAI,EAAG,KAAK,WAAa,CAAW,CAClD,CAEA,YAAY,EAAoB,CAC9B,IAAA,EAAIC,EAAAA,WAAAA,CAAW,EAAM,QAAQ,IAAA,EAAKA,EAAAA,WAAAA,CAAW,EAAM,QAAQ,EAAG,CAC5D,KAAK,KAAK,IAAA,EAAS,EACnB,MACF,CACA,IAAM,EAAU,KAAK,QAAQ,EAC7B,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,OAAO,EAAG,CAC7B,IAAM,EAAM,EAAQ,KAAK,IAAI,KAAK,SAAU,EAAQ,OAAS,CAAC,GAC1D,GAAK,KAAK,KAAK,EAAI,KAAK,EAC5B,MACF,CAEA,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,IAAI,IAAA,EAAKA,EAAAA,WAAAA,CAAW,EAAM,QAAQ,EAAG,KAAK,KAAK,GAAI,EAAQ,MAAM,OACjF,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,MAAM,IAAA,EAAKA,EAAAA,WAAAA,CAAW,EAAM,QAAQ,EAAG,KAAK,KAAK,EAAG,EAAQ,MAAM,OACvF,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,QAAQ,EAAG,KAAK,KAAK,CAAC,KAAK,SAAS,EAAG,EAAQ,MAAM,OAC1E,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,UAAU,EAAG,KAAK,KAAK,KAAK,SAAS,EAAG,EAAQ,MAAM,OAC3E,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,WAAW,EAAG,KAAK,SAAS,KAAK,MAAM,MAAM,EAAG,EAAE,CAAC,OACxE,GAAI,IAAS,IAAW,KAAK,SAAS,EAAE,OACxC,GAAI,CAACC,EAAAA,eAAe,CAAI,EAAG,KAAK,SAAS,KAAK,MAAQ,CAAI,OAC1D,OAEL,KAAK,IAAI,cAAc,CACzB,CAGA,SAAiB,EAAqB,CAChC,IAAU,KAAK,QACnB,KAAK,MAAQ,EACb,KAAK,SAAW,EAClB,CAEA,KAAa,EAAe,EAAqB,CAC3C,IAAU,IACd,KAAK,SAAW,KAAK,IAAI,EAAG,KAAK,IAAI,EAAQ,EAAG,KAAK,SAAW,CAAK,CAAC,EACxE,CAEA,WAAyC,CACvC,IAAM,EAAU,KAAK,QAAQ,EACvB,EAAQ,KAAK,MAAQ,GAAG,EAAQ,OAAO,MAAM,KAAK,KAAK,SAAW,GAAG,KAAK,KAAK,SACrF,MAAO,CACL,MAAO,KAAK,OAAO,MACnB,OAAQC,EAAAA,oBACR,WAAY,KAAK,OAAO,WACxB,YAAa,GAAG,EAAM,GAAG,KAAK,OAAO,OACrC,OAAQ,qBAAqB,KAAK,OAAO,OAAO,+CAChD,YAAa,CACX,CAAC,KAAM,QAAQ,EACf,CAAC,QAAS,KAAK,OAAO,MAAM,EAC5B,CAAC,OAAQ,QAAQ,EACjB,CAAC,SAAU,OAAO,EAClB,CAAC,MAAO,QAAQ,CAClB,EACA,YACE,EAAQ,OAAS,EAAI,GAAG,KAAK,IAAI,KAAK,SAAU,EAAQ,OAAS,CAAC,EAAI,EAAE,GAAG,EAAQ,SAAW,UAClG,CACF,CAEA,WAAqB,EAAe,EAA0B,CAE5D,MADA,MAAK,WAAa,EACX,CAAC,GAAG,KAAK,YAAY,CAAK,EAAG,GAAG,KAAK,UAAU,CAAK,CAAC,CAAC,CAAC,MAAM,EAAG,CAAM,CAC/E,CAEA,YAAoB,EAAyB,CAC3C,IAAM,EAAQ,KAAK,MACf,GAAG,KAAK,SACR,IAAkB,KAAK,MAAM,GAAG,MAAK,KAAK,OAAO,iBAAiB,IACtE,MAAO,CAAC,KAAK,MAAM,GAAG,MAAK,QAAY,EAAG,KAAK,MAAM,GAAG,EAAaC,EAAAA,IAAI,IAAI,IAAS,CAAK,CAAC,EAAG,EAAE,CACnG,CAEA,UAAkB,EAAyB,CACzC,GAAI,KAAK,KAAK,SAAW,EAAG,MAAO,CAAC,KAAK,MAAM,GAAG,MAAK,KAAK,OAAO,YAAY,CAAC,EAChF,IAAM,EAAU,KAAK,QAAQ,EAC7B,GAAI,EAAQ,SAAW,EAAG,MAAO,CAAC,KAAK,MAAM,GAAG,MAAK,MAAM,KAAK,OAAO,KAAK,UAAU,KAAK,MAAM,GAAG,CAAC,EAErG,IAAM,EAAW,KAAK,SAAS,EACzB,EAAS,KAAK,IAAI,KAAK,SAAU,EAAQ,OAAS,CAAC,EAGnD,EAAQ,KAAK,IAAI,EAAG,KAAK,IAAI,EAAS,EAAW,EAAG,KAAK,IAAI,EAAG,EAAQ,OAAS,CAAQ,CAAC,CAAC,EAE3F,EAAY,KAAK,IAAI,GAAgB,KAAK,OAAO,EAAQ,EAAiB,GAAc,GAAU,CAAC,EACnG,EAAc,KAAK,IAAI,EAAG,EAAQ,EAAiB,EAAa,CAAS,EAC/E,OAAO,EAAQ,MAAM,EAAO,EAAQ,CAAQ,CAAC,CAAC,KAAK,EAAK,IAAW,CACjE,IAAM,EAAU,EAAQ,IAAW,EAC7B,EAAS,EAAU,KAAK,MAAM,GAAG,SAAA,GAAwB,EAAI,IAC7D,EAAO,EAAI,MAAQ,YACnB,EAAQ,EAAU,KAAK,MAAM,KAAKA,EAAAA,IAAI,EAAM,CAAS,CAAC,EAAIA,EAAAA,IAAI,EAAM,CAAS,EAC7E,EAAS,KAAK,MAAM,GAAG,MAAKA,EAAAA,IAAIC,EAAAA,SAAS,EAAI,OAAQ,CAAW,EAAG,CAAW,CAAC,EAC/E,EAAsB,EAAU,EAAc,gBACpD,OAAO,KAAK,MAAM,GAAG,EAAYD,EAAAA,IAAI,GAAG,EAAO,GAAG,IAAQ,IAAI,OAAO,CAAU,IAAI,IAAU,CAAK,CAAC,CACrG,CAAC,CACH,CACF,EAEA,eAAsB,EACpB,EACA,EACA,EACwB,CACxB,OAAO,EAAI,GAAG,QACX,EAAK,EAAO,EAAc,IAAS,IAAI,EAAwB,EAAK,EAAO,EAAQ,EAAM,CAAI,EAC9FN,EAAAA,0BACF,CACF"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowPicker.mjs","names":[],"sources":["../../../src/tui/workflow/workflowPicker.ts"],"sourcesContent":["/**\n * Scrollable, filterable list picker for every long list this extension shows.\n *\n * Replaces a bare `ui.select`, which wrapped each `name · detail` pair across\n * two or three terminal rows and left no way to reach a row other than scrolling\n * past all of them. This is a doom overlay instead: one row per item, a windowed\n * list that scrolls with the selection, and a filter field that narrows as you\n * type.\n *\n * Generic over the value it hands back, so `SPC w w` picks a catalog entry and\n * `SPC w r` picks a failed run through the same surface.\n *\n * Rendering is pure, so the surface can be asserted as text without a terminal.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey } from '@earendil-works/pi-tui';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay.ts';\nimport { CURSOR_BLOCK, fit, isControlInput, matchesQuery, pathTail, SELECTION_MARKER } from './overlayText';\n\nexport const WORKFLOW_PICKER_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst FILTER_LABEL = 'FILTER';\nconst EMPTY_NAME = '(unnamed)';\nconst KEY_CLEAR = '\\x15';\n/** Rows the filter field and its trailing spacer take from the body. */\nconst FILTER_ROWS = 3;\n/** Columns the marker and its trailing space take from a row. */\nconst MARKER_COLUMNS = 2;\n/** Blank columns between the name column and the detail column. */\nconst COLUMN_GAP = 2;\nconst MIN_NAME_WIDTH = 16;\nconst NAME_RATIO = 0.45;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\nconst DIM: ThemeColor = 'dim';\nconst ACCENT: ThemeColor = 'accent';\nconst SELECTED_BG: ThemeBg = 'selectedBg';\nconst ROW_BG: ThemeBg = 'userMessageBg';\n\nexport interface PickerRow<T> {\n /** Stable identity, used only to keep rows distinct. */\n key: string;\n /** Left column. */\n name: string;\n /** Right column, trimmed from the front so its tail survives. */\n detail: string;\n /** Extra text the filter should match but the row does not show. */\n search?: string;\n value: T;\n}\n\nexport interface WorkflowPickerConfig {\n title: string;\n breadcrumb: string;\n /** Plural noun for the header count, e.g. `workflows`. */\n unit: string;\n /** Verb on the enter key cap, e.g. `launch`. */\n action: string;\n filterPlaceholder: string;\n /** Shown when the source list itself is empty, as opposed to over-filtered. */\n emptyMessage: string;\n}\n\nexport function filterPickerRows<T>(rows: readonly PickerRow<T>[], query: string): PickerRow<T>[] {\n if (!query.trim()) return [...rows];\n return rows.filter((row) => matchesQuery([row.name, row.detail, row.search ?? ''].join(' '), query));\n}\n\nexport class WorkflowPickerComponent<T> extends DoomOverlay {\n private query = '';\n private selected = 0;\n private bodyHeight = 1;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly config: WorkflowPickerConfig,\n private readonly rows: readonly PickerRow<T>[],\n private readonly done: (value: T | undefined) => void,\n ) {\n super(tui, theme);\n }\n\n private matches(): PickerRow<T>[] {\n return filterPickerRows(this.rows, this.query);\n }\n\n /** Rows the list can draw, once the filter field has taken its share. */\n private capacity(): number {\n return Math.max(1, this.bodyHeight - FILTER_ROWS);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n const matches = this.matches();\n if (matchesKey(data, 'enter')) {\n const row = matches[Math.min(this.selected, matches.length - 1)];\n if (row) this.done(row.value);\n return;\n }\n\n if (matchesKey(data, 'up') || matchesKey(data, 'ctrl+p')) this.move(-1, matches.length);\n else if (matchesKey(data, 'down') || matchesKey(data, 'ctrl+n')) this.move(1, matches.length);\n else if (matchesKey(data, 'pageUp')) this.move(-this.capacity(), matches.length);\n else if (matchesKey(data, 'pageDown')) this.move(this.capacity(), matches.length);\n else if (matchesKey(data, 'backspace')) this.setQuery(this.query.slice(0, -1));\n else if (data === KEY_CLEAR) this.setQuery('');\n else if (!isControlInput(data)) this.setQuery(this.query + data);\n else return;\n\n this.tui.requestRender();\n }\n\n /** Editing the filter resets the cursor: the previous index named another row. */\n private setQuery(query: string): void {\n if (query === this.query) return;\n this.query = query;\n this.selected = 0;\n }\n\n private move(delta: number, count: number): void {\n if (count === 0) return;\n this.selected = Math.max(0, Math.min(count - 1, this.selected + delta));\n }\n\n protected getChrome(): DoomOverlayChrome {\n const matches = this.matches();\n const scope = this.query ? `${matches.length} of ${this.rows.length}` : `${this.rows.length}`;\n return {\n title: this.config.title,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.config.breadcrumb,\n headerRight: `${scope} ${this.config.unit}`,\n footer: `↑↓ select · enter ${this.config.action} · type to filter · ctrl+u clear · esc cancel`,\n footerHints: [\n ['↑↓', 'select'],\n ['enter', this.config.action],\n ['type', 'filter'],\n ['ctrl+u', 'clear'],\n ['esc', 'cancel'],\n ],\n footerRight:\n matches.length > 0 ? `${Math.min(this.selected, matches.length - 1) + 1}/${matches.length}` : 'no match',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n this.bodyHeight = height;\n return [...this.filterField(width), ...this.listLines(width)].slice(0, height);\n }\n\n private filterField(width: number): string[] {\n const value = this.query\n ? `${this.query}${CURSOR_BLOCK}`\n : `${CURSOR_BLOCK}${this.theme.fg(DIM, this.config.filterPlaceholder)}`;\n return [this.theme.fg(DIM, FILTER_LABEL), this.theme.bg(SELECTED_BG, fit(` ${value}`, width)), ''];\n }\n\n private listLines(width: number): string[] {\n if (this.rows.length === 0) return [this.theme.fg(DIM, this.config.emptyMessage)];\n const matches = this.matches();\n if (matches.length === 0) return [this.theme.fg(DIM, `No ${this.config.unit} match \"${this.query}\".`)];\n\n const capacity = this.capacity();\n const active = Math.min(this.selected, matches.length - 1);\n // The window follows the cursor rather than the other way round, so a filter\n // that shrinks the list can never strand the selection off-screen.\n const start = Math.max(0, Math.min(active - capacity + 1, Math.max(0, matches.length - capacity)));\n\n const nameWidth = Math.max(MIN_NAME_WIDTH, Math.floor((width - MARKER_COLUMNS - COLUMN_GAP) * NAME_RATIO));\n const detailWidth = Math.max(0, width - MARKER_COLUMNS - COLUMN_GAP - nameWidth);\n return matches.slice(start, start + capacity).map((row, offset) => {\n const current = start + offset === active;\n const marker = current ? this.theme.fg(ACCENT, SELECTION_MARKER) : ' ';\n const name = row.name || EMPTY_NAME;\n const label = current ? this.theme.bold(fit(name, nameWidth)) : fit(name, nameWidth);\n const detail = this.theme.fg(DIM, fit(pathTail(row.detail, detailWidth), detailWidth));\n const background: ThemeBg = current ? SELECTED_BG : ROW_BG;\n return this.theme.bg(background, fit(`${marker} ${label}${' '.repeat(COLUMN_GAP)}${detail}`, width));\n });\n }\n}\n\nexport async function openWorkflowPickerOverlay<T>(\n ctx: ExtensionContext,\n config: WorkflowPickerConfig,\n rows: readonly PickerRow<T>[],\n): Promise<T | undefined> {\n return ctx.ui.custom<T | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowPickerComponent(tui, theme, config, rows, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ let e=require("@earendil-works/pi-tui");const t=`workflow-mcp-progress`,n=[`⠋`,`⠙`,`⠹`,`⠸`,`⠼`,`⠴`,`⠦`,`⠧`,`⠇`,`⠏`],r={starting:`dim`,running:`accent`,completed:`success`,paused:`warning`,failed:`error`,skipped:`muted`};function i(e){return e.findLast(e=>e.type===`step`&&!!e.step)}function a(e){return e.at(-1)}function o(e,t){if(e.stage===`error`)return`failed`;if(e.stage===`completed`)return`completed`;if(e.executionState===`paused`||e.executionState===`pause_requested`)return`paused`;switch(t?.status){case`failed`:return`failed`;case`paused`:case`pause_requested`:return`paused`;case`completed`:return`completed`;case`skipped`:return`skipped`;case`resumed`:case`running`:return`running`;default:return e.executionCursor?.stepName?`running`:`starting`}}function s(e){switch(e.executionCursor?.phase){case`pre`:return`Preparing workflow`;case`post`:return`Finalizing workflow`;case`job`:case`step`:return`Starting job`;default:return`Starting workflow`}}function c(e,t){let n=i(t),r=a(t),c=e.executionCursor,l=c?.job??n?.job??r?.job??e.job??c?.phase??`workflow`,u=c?.stepName??(n?.job===l?n.step:void 0)??s(e);return{job:l,key:e.runId??`${e.workspace}/${e.runKey}`,name:e.displayName,startedAt:e.startedAt,state:o(e,r),step:u}}function l(e){if(!Number.isFinite(e)||e<0)return`?`;let t=Math.floor(e/1e3),n=t%60,r=Math.floor(t/60);if(r<60)return r===0?`${n}s`:`${r}m${String(n).padStart(2,`0`)}s`;let i=r%60;return`${Math.floor(r/60)}h${String(i).padStart(2,`0`)}m${String(n).padStart(2,`0`)}s`}function u(e,t){if(e!==`running`&&e!==`starting`)return`*`;let r=Number.isFinite(t)?Math.floor(Math.max(0,t)/200):0;return n[r%n.length]??n[0]}function d(e,t,n=Date.now()){let i=Date.parse(e.startedAt),a=l(Number.isFinite(i)?n-i:NaN),o=r[e.state];return[t.fg(o,e.name),t.fg(`muted`,`[${a}][${e.job}]`),t.fg(o,` ${u(e.state,n)} `),t.fg(`text`,e.step)].join(``)}var f=class{rows=[];uiCtx;widgetRegistered=!1;tui;progressTimer;setUICtx(e){e!==this.uiCtx&&(this.uiCtx&&this.widgetRegistered&&this.uiCtx.setWidget(t,void 0),this.stopProgressTimer(),this.uiCtx=e,this.widgetRegistered=!1,this.tui=void 0)}update(e){if(this.rows=e,!this.uiCtx){this.stopProgressTimer();return}if(e.length===0){this.widgetRegistered&&(this.uiCtx.setWidget(t,void 0),this.widgetRegistered=!1,this.tui=void 0),this.stopProgressTimer();return}if(this.widgetRegistered){this.syncProgressTimer(),this.tui?.requestRender();return}this.uiCtx.setWidget(t,(e,t)=>(this.tui=e,{render:e=>this.renderWidget(this.uiCtx?.theme??t,e),invalidate:()=>{}}),{placement:`aboveEditor`}),this.widgetRegistered=!0,this.syncProgressTimer()}syncProgressTimer(){if(!this.widgetRegistered||this.rows.length===0){this.stopProgressTimer();return}this.progressTimer||(this.progressTimer=setInterval(()=>this.tui?.requestRender(),200),this.progressTimer.unref?.())}stopProgressTimer(){this.progressTimer&&clearInterval(this.progressTimer),this.progressTimer=void 0}renderWidget(t,n){if(this.rows.length===0)return[];let r=t=>(0,e.truncateToWidth)(t,n,`…`),i=r(`${t.fg(`accent`,`●`)} ${t.fg(`accent`,`Workflows (${this.rows.length})`)}`),a=Date.now(),o=[i];for(let[e,n]of this.rows.entries()){let i=e===this.rows.length-1?`└─`:`├─`;o.push(r(`${t.fg(`dim`,i)} ${d(n,t,a)}`))}return o.push(``),o}dispose(){this.stopProgressTimer(),this.uiCtx&&this.uiCtx.setWidget(t,void 0),this.rows=[],this.widgetRegistered=!1,this.tui=void 0,this.uiCtx=void 0}};exports.WORKFLOW_PROGRESS_COLORS=r,exports.WORKFLOW_PROGRESS_WIDGET_KEY=t,exports.WorkflowProgressOverlay=f,exports.formatWorkflowDuration=l,exports.formatWorkflowProgressRow=d,exports.workflowProgressRow=c;
2
+ //# sourceMappingURL=workflowProgressOverlay.cjs.map
@@ -0,0 +1 @@
1
+ {"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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"}
@@ -0,0 +1,2 @@
1
+ import{truncateToWidth as e}from"@earendil-works/pi-tui";const t=`workflow-mcp-progress`,n=[`⠋`,`⠙`,`⠹`,`⠸`,`⠼`,`⠴`,`⠦`,`⠧`,`⠇`,`⠏`],r={starting:`dim`,running:`accent`,completed:`success`,paused:`warning`,failed:`error`,skipped:`muted`};function i(e){return e.findLast(e=>e.type===`step`&&!!e.step)}function a(e){return e.at(-1)}function o(e,t){if(e.stage===`error`)return`failed`;if(e.stage===`completed`)return`completed`;if(e.executionState===`paused`||e.executionState===`pause_requested`)return`paused`;switch(t?.status){case`failed`:return`failed`;case`paused`:case`pause_requested`:return`paused`;case`completed`:return`completed`;case`skipped`:return`skipped`;case`resumed`:case`running`:return`running`;default:return e.executionCursor?.stepName?`running`:`starting`}}function s(e){switch(e.executionCursor?.phase){case`pre`:return`Preparing workflow`;case`post`:return`Finalizing workflow`;case`job`:case`step`:return`Starting job`;default:return`Starting workflow`}}function c(e,t){let n=i(t),r=a(t),c=e.executionCursor,l=c?.job??n?.job??r?.job??e.job??c?.phase??`workflow`,u=c?.stepName??(n?.job===l?n.step:void 0)??s(e);return{job:l,key:e.runId??`${e.workspace}/${e.runKey}`,name:e.displayName,startedAt:e.startedAt,state:o(e,r),step:u}}function l(e){if(!Number.isFinite(e)||e<0)return`?`;let t=Math.floor(e/1e3),n=t%60,r=Math.floor(t/60);if(r<60)return r===0?`${n}s`:`${r}m${String(n).padStart(2,`0`)}s`;let i=r%60;return`${Math.floor(r/60)}h${String(i).padStart(2,`0`)}m${String(n).padStart(2,`0`)}s`}function u(e,t){if(e!==`running`&&e!==`starting`)return`*`;let r=Number.isFinite(t)?Math.floor(Math.max(0,t)/200):0;return n[r%n.length]??n[0]}function d(e,t,n=Date.now()){let i=Date.parse(e.startedAt),a=l(Number.isFinite(i)?n-i:NaN),o=r[e.state];return[t.fg(o,e.name),t.fg(`muted`,`[${a}][${e.job}]`),t.fg(o,` ${u(e.state,n)} `),t.fg(`text`,e.step)].join(``)}var f=class{rows=[];uiCtx;widgetRegistered=!1;tui;progressTimer;setUICtx(e){e!==this.uiCtx&&(this.uiCtx&&this.widgetRegistered&&this.uiCtx.setWidget(t,void 0),this.stopProgressTimer(),this.uiCtx=e,this.widgetRegistered=!1,this.tui=void 0)}update(e){if(this.rows=e,!this.uiCtx){this.stopProgressTimer();return}if(e.length===0){this.widgetRegistered&&(this.uiCtx.setWidget(t,void 0),this.widgetRegistered=!1,this.tui=void 0),this.stopProgressTimer();return}if(this.widgetRegistered){this.syncProgressTimer(),this.tui?.requestRender();return}this.uiCtx.setWidget(t,(e,t)=>(this.tui=e,{render:e=>this.renderWidget(this.uiCtx?.theme??t,e),invalidate:()=>{}}),{placement:`aboveEditor`}),this.widgetRegistered=!0,this.syncProgressTimer()}syncProgressTimer(){if(!this.widgetRegistered||this.rows.length===0){this.stopProgressTimer();return}this.progressTimer||(this.progressTimer=setInterval(()=>this.tui?.requestRender(),200),this.progressTimer.unref?.())}stopProgressTimer(){this.progressTimer&&clearInterval(this.progressTimer),this.progressTimer=void 0}renderWidget(t,n){if(this.rows.length===0)return[];let r=t=>e(t,n,`…`),i=r(`${t.fg(`accent`,`●`)} ${t.fg(`accent`,`Workflows (${this.rows.length})`)}`),a=Date.now(),o=[i];for(let[e,n]of this.rows.entries()){let i=e===this.rows.length-1?`└─`:`├─`;o.push(r(`${t.fg(`dim`,i)} ${d(n,t,a)}`))}return o.push(``),o}dispose(){this.stopProgressTimer(),this.uiCtx&&this.uiCtx.setWidget(t,void 0),this.rows=[],this.widgetRegistered=!1,this.tui=void 0,this.uiCtx=void 0}};export{r as WORKFLOW_PROGRESS_COLORS,t as WORKFLOW_PROGRESS_WIDGET_KEY,f as WorkflowProgressOverlay,l as formatWorkflowDuration,d as formatWorkflowProgressRow,c as workflowProgressRow};
2
+ //# sourceMappingURL=workflowProgressOverlay.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowProgressOverlay.mjs","names":[],"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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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowPrompt.cjs","names":["DOOM_FULLSCREEN_UI_OPTIONS","DoomOverlay","matchesKey","DOOM_OVERLAY_ACCENT","fit","rightAligned","isControlInput"],"sources":["../../../src/tui/workflow/workflowPrompt.ts"],"sourcesContent":["/**\n * The short prompts: pick one of a few options, or type one value.\n *\n * These were `ui.select` and `ui.input`, which meant the run manager, the runner\n * picker and every `workflow_dispatch` input rendered as native Pi widgets in the\n * middle of a flow whose other steps are framed doom overlays. Same surface for\n * all of them now, so a launch reads as one sequence rather than three styles.\n *\n * Rendering is pure, so both modes can be asserted as text without a terminal.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey } from '@earendil-works/pi-tui';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay.ts';\nimport { CURSOR_BLOCK, fit, isControlInput, rightAligned, SELECTION_MARKER } from './overlayText';\n\nexport const WORKFLOW_PROMPT_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst CHOICE_TITLE = 'WORKFLOW PROMPT';\nconst CHOOSE_LABEL = 'CHOOSE';\nconst VALUE_LABEL = 'VALUE';\nconst DEFAULT_MARKER = 'default';\nconst EMPTY_CHOICES = 'No options are available.';\nconst CHOICE_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'select'],\n ['enter', 'confirm'],\n ['esc', 'cancel'],\n];\nconst INPUT_HINTS: readonly (readonly [string, string])[] = [\n ['type', 'edit'],\n ['enter', 'confirm'],\n ['ctrl+u', 'clear'],\n ['esc', 'cancel'],\n];\nconst KEY_CLEAR = '\\x15';\n/** Rows the heading and its spacer take before the first option. */\nconst HEADING_ROWS = 2;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\nconst DIM: ThemeColor = 'dim';\nconst ACCENT: ThemeColor = 'accent';\nconst SELECTED_BG: ThemeBg = 'selectedBg';\nconst ROW_BG: ThemeBg = 'userMessageBg';\n\nexport interface WorkflowChoiceOptions {\n title: string;\n breadcrumb: string;\n choices: readonly string[];\n /** Marked `default` in the list and pre-selected. */\n preselect?: string;\n}\n\nexport interface WorkflowInputOptions {\n title: string;\n breadcrumb: string;\n /** Committed when the field is left empty, and shown beside the label. */\n fallback?: string;\n}\n\nexport class WorkflowChoiceComponent extends DoomOverlay {\n private selected: number;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: WorkflowChoiceOptions,\n private readonly done: (choice: string | undefined) => void,\n ) {\n super(tui, theme);\n this.selected = Math.max(0, options.preselect ? options.choices.indexOf(options.preselect) : 0);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, 'enter')) {\n const choice = this.options.choices[this.selected];\n if (choice !== undefined) this.done(choice);\n return;\n }\n const count = this.options.choices.length;\n if (count === 0) return;\n if (matchesKey(data, 'up') || matchesKey(data, 'ctrl+p')) this.selected = Math.max(0, this.selected - 1);\n else if (matchesKey(data, 'down') || matchesKey(data, 'ctrl+n'))\n this.selected = Math.min(count - 1, this.selected + 1);\n else return;\n this.tui.requestRender();\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: CHOICE_TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.options.breadcrumb,\n headerRight: `${this.options.choices.length} options`,\n footer: '↑↓ select · enter confirm · esc cancel',\n footerHints: CHOICE_HINTS,\n footerRight:\n this.options.choices.length > 0 ? `${this.selected + 1}/${this.options.choices.length}` : 'no options',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const lines = [this.theme.bold(fit(this.options.title, width)), ''];\n if (this.options.choices.length === 0) {\n lines.push(this.theme.fg(DIM, EMPTY_CHOICES));\n return lines.slice(0, height);\n }\n\n lines.push(this.theme.fg(DIM, CHOOSE_LABEL));\n // The window follows the cursor, so a list longer than the body still scrolls.\n const capacity = Math.max(1, height - HEADING_ROWS - 1);\n const start = Math.max(\n 0,\n Math.min(this.selected - capacity + 1, Math.max(0, this.options.choices.length - capacity)),\n );\n for (const [offset, choice] of this.options.choices.slice(start, start + capacity).entries()) {\n const current = start + offset === this.selected;\n const marker = current ? this.theme.fg(ACCENT, SELECTION_MARKER) : ' ';\n const label = current ? this.theme.bold(choice) : choice;\n const note = choice === this.options.preselect ? this.theme.fg(DIM, DEFAULT_MARKER) : '';\n const background: ThemeBg = current ? SELECTED_BG : ROW_BG;\n lines.push(this.theme.bg(background, rightAligned(`${marker} ${label}`, note, width)));\n }\n return lines.slice(0, height);\n }\n}\n\nexport class WorkflowInputComponent extends DoomOverlay {\n private draft = '';\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: WorkflowInputOptions,\n private readonly done: (value: string | undefined) => void,\n ) {\n super(tui, theme);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, 'enter')) {\n // An empty field commits the fallback rather than an empty string: the\n // caller offered a default, and pressing enter is how you accept it.\n this.done(this.draft.trim() || this.options.fallback || '');\n return;\n }\n if (matchesKey(data, 'backspace')) this.draft = this.draft.slice(0, -1);\n else if (data === KEY_CLEAR) this.draft = '';\n else if (!isControlInput(data)) this.draft += data;\n else return;\n this.tui.requestRender();\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: CHOICE_TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.options.breadcrumb,\n headerRight: this.options.fallback ? `default ${this.options.fallback}` : 'no default',\n footer: 'type to edit · enter confirm · ctrl+u clear · esc cancel',\n footerHints: INPUT_HINTS,\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const label = this.options.fallback\n ? rightAligned(\n this.theme.fg(DIM, VALUE_LABEL),\n this.theme.fg(DIM, `${DEFAULT_MARKER} ${this.options.fallback}`),\n width,\n )\n : this.theme.fg(DIM, VALUE_LABEL);\n return [\n this.theme.bold(fit(this.options.title, width)),\n '',\n label,\n this.theme.bg(SELECTED_BG, fit(` ${this.draft}${CURSOR_BLOCK}`, width)),\n ].slice(0, height);\n }\n}\n\nexport async function openWorkflowChoice(\n ctx: ExtensionContext,\n options: WorkflowChoiceOptions,\n): Promise<string | undefined> {\n return ctx.ui.custom<string | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowChoiceComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n\nexport async function openWorkflowInput(\n ctx: ExtensionContext,\n options: WorkflowInputOptions,\n): Promise<string | undefined> {\n return ctx.ui.custom<string | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowInputComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowPrompt.mjs","names":[],"sources":["../../../src/tui/workflow/workflowPrompt.ts"],"sourcesContent":["/**\n * The short prompts: pick one of a few options, or type one value.\n *\n * These were `ui.select` and `ui.input`, which meant the run manager, the runner\n * picker and every `workflow_dispatch` input rendered as native Pi widgets in the\n * middle of a flow whose other steps are framed doom overlays. Same surface for\n * all of them now, so a launch reads as one sequence rather than three styles.\n *\n * Rendering is pure, so both modes can be asserted as text without a terminal.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { matchesKey } from '@earendil-works/pi-tui';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay.ts';\nimport { CURSOR_BLOCK, fit, isControlInput, rightAligned, SELECTION_MARKER } from './overlayText';\n\nexport const WORKFLOW_PROMPT_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst CHOICE_TITLE = 'WORKFLOW PROMPT';\nconst CHOOSE_LABEL = 'CHOOSE';\nconst VALUE_LABEL = 'VALUE';\nconst DEFAULT_MARKER = 'default';\nconst EMPTY_CHOICES = 'No options are available.';\nconst CHOICE_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'select'],\n ['enter', 'confirm'],\n ['esc', 'cancel'],\n];\nconst INPUT_HINTS: readonly (readonly [string, string])[] = [\n ['type', 'edit'],\n ['enter', 'confirm'],\n ['ctrl+u', 'clear'],\n ['esc', 'cancel'],\n];\nconst KEY_CLEAR = '\\x15';\n/** Rows the heading and its spacer take before the first option. */\nconst HEADING_ROWS = 2;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\nconst DIM: ThemeColor = 'dim';\nconst ACCENT: ThemeColor = 'accent';\nconst SELECTED_BG: ThemeBg = 'selectedBg';\nconst ROW_BG: ThemeBg = 'userMessageBg';\n\nexport interface WorkflowChoiceOptions {\n title: string;\n breadcrumb: string;\n choices: readonly string[];\n /** Marked `default` in the list and pre-selected. */\n preselect?: string;\n}\n\nexport interface WorkflowInputOptions {\n title: string;\n breadcrumb: string;\n /** Committed when the field is left empty, and shown beside the label. */\n fallback?: string;\n}\n\nexport class WorkflowChoiceComponent extends DoomOverlay {\n private selected: number;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: WorkflowChoiceOptions,\n private readonly done: (choice: string | undefined) => void,\n ) {\n super(tui, theme);\n this.selected = Math.max(0, options.preselect ? options.choices.indexOf(options.preselect) : 0);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, 'enter')) {\n const choice = this.options.choices[this.selected];\n if (choice !== undefined) this.done(choice);\n return;\n }\n const count = this.options.choices.length;\n if (count === 0) return;\n if (matchesKey(data, 'up') || matchesKey(data, 'ctrl+p')) this.selected = Math.max(0, this.selected - 1);\n else if (matchesKey(data, 'down') || matchesKey(data, 'ctrl+n'))\n this.selected = Math.min(count - 1, this.selected + 1);\n else return;\n this.tui.requestRender();\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: CHOICE_TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.options.breadcrumb,\n headerRight: `${this.options.choices.length} options`,\n footer: '↑↓ select · enter confirm · esc cancel',\n footerHints: CHOICE_HINTS,\n footerRight:\n this.options.choices.length > 0 ? `${this.selected + 1}/${this.options.choices.length}` : 'no options',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const lines = [this.theme.bold(fit(this.options.title, width)), ''];\n if (this.options.choices.length === 0) {\n lines.push(this.theme.fg(DIM, EMPTY_CHOICES));\n return lines.slice(0, height);\n }\n\n lines.push(this.theme.fg(DIM, CHOOSE_LABEL));\n // The window follows the cursor, so a list longer than the body still scrolls.\n const capacity = Math.max(1, height - HEADING_ROWS - 1);\n const start = Math.max(\n 0,\n Math.min(this.selected - capacity + 1, Math.max(0, this.options.choices.length - capacity)),\n );\n for (const [offset, choice] of this.options.choices.slice(start, start + capacity).entries()) {\n const current = start + offset === this.selected;\n const marker = current ? this.theme.fg(ACCENT, SELECTION_MARKER) : ' ';\n const label = current ? this.theme.bold(choice) : choice;\n const note = choice === this.options.preselect ? this.theme.fg(DIM, DEFAULT_MARKER) : '';\n const background: ThemeBg = current ? SELECTED_BG : ROW_BG;\n lines.push(this.theme.bg(background, rightAligned(`${marker} ${label}`, note, width)));\n }\n return lines.slice(0, height);\n }\n}\n\nexport class WorkflowInputComponent extends DoomOverlay {\n private draft = '';\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: WorkflowInputOptions,\n private readonly done: (value: string | undefined) => void,\n ) {\n super(tui, theme);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, 'enter')) {\n // An empty field commits the fallback rather than an empty string: the\n // caller offered a default, and pressing enter is how you accept it.\n this.done(this.draft.trim() || this.options.fallback || '');\n return;\n }\n if (matchesKey(data, 'backspace')) this.draft = this.draft.slice(0, -1);\n else if (data === KEY_CLEAR) this.draft = '';\n else if (!isControlInput(data)) this.draft += data;\n else return;\n this.tui.requestRender();\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: CHOICE_TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.options.breadcrumb,\n headerRight: this.options.fallback ? `default ${this.options.fallback}` : 'no default',\n footer: 'type to edit · enter confirm · ctrl+u clear · esc cancel',\n footerHints: INPUT_HINTS,\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const label = this.options.fallback\n ? rightAligned(\n this.theme.fg(DIM, VALUE_LABEL),\n this.theme.fg(DIM, `${DEFAULT_MARKER} ${this.options.fallback}`),\n width,\n )\n : this.theme.fg(DIM, VALUE_LABEL);\n return [\n this.theme.bold(fit(this.options.title, width)),\n '',\n label,\n this.theme.bg(SELECTED_BG, fit(` ${this.draft}${CURSOR_BLOCK}`, width)),\n ].slice(0, height);\n }\n}\n\nexport async function openWorkflowChoice(\n ctx: ExtensionContext,\n options: WorkflowChoiceOptions,\n): Promise<string | undefined> {\n return ctx.ui.custom<string | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowChoiceComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n\nexport async function openWorkflowInput(\n ctx: ExtensionContext,\n options: WorkflowInputOptions,\n): Promise<string | undefined> {\n return ctx.ui.custom<string | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowInputComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"2OAsB+C,EAA2B,eAE1E,MAAM,EAAe,kBAEf,EAAc,QACd,EAAiB,UAEjB,EAAuD,CAC3D,CAAC,KAAM,QAAQ,EACf,CAAC,QAAS,SAAS,EACnB,CAAC,MAAO,QAAQ,CAClB,EACM,EAAsD,CAC1D,CAAC,OAAQ,MAAM,EACf,CAAC,QAAS,SAAS,EACnB,CAAC,SAAU,OAAO,EAClB,CAAC,MAAO,QAAQ,CAClB,EAWM,EAAuB,aAkB7B,IAAa,EAAb,cAA6C,CAAY,CAMpC,QACA,KANnB,SAEA,YACE,EACA,EACA,EACA,EACA,CACA,MAAM,EAAK,CAAK,EAHC,KAAA,QAAA,EACA,KAAA,KAAA,EAGjB,KAAK,SAAW,KAAK,IAAI,EAAG,EAAQ,UAAY,EAAQ,QAAQ,QAAQ,EAAQ,SAAS,EAAI,CAAC,CAChG,CAEA,YAAY,EAAoB,CAC9B,GAAI,EAAW,EAAM,QAAQ,GAAK,EAAW,EAAM,QAAQ,EAAG,CAC5D,KAAK,KAAK,IAAA,EAAS,EACnB,MACF,CACA,GAAI,EAAW,EAAM,OAAO,EAAG,CAC7B,IAAM,EAAS,KAAK,QAAQ,QAAQ,KAAK,UACrC,IAAW,IAAA,IAAW,KAAK,KAAK,CAAM,EAC1C,MACF,CACA,IAAM,EAAQ,KAAK,QAAQ,QAAQ,OAC/B,OAAU,EACd,IAAI,EAAW,EAAM,IAAI,GAAK,EAAW,EAAM,QAAQ,EAAG,KAAK,SAAW,KAAK,IAAI,EAAG,KAAK,SAAW,CAAC,OAClG,GAAI,EAAW,EAAM,MAAM,GAAK,EAAW,EAAM,QAAQ,EAC5D,KAAK,SAAW,KAAK,IAAI,EAAQ,EAAG,KAAK,SAAW,CAAC,OAClD,OACL,KAAK,IAAI,cAAc,CADlB,CAEP,CAEA,WAAyC,CACvC,MAAO,CACL,MAAO,EACP,OAAQ,EACR,WAAY,KAAK,QAAQ,WACzB,YAAa,GAAG,KAAK,QAAQ,QAAQ,OAAO,UAC5C,OAAQ,yCACR,YAAa,EACb,YACE,KAAK,QAAQ,QAAQ,OAAS,EAAI,GAAG,KAAK,SAAW,EAAE,GAAG,KAAK,QAAQ,QAAQ,SAAW,YAC9F,CACF,CAEA,WAAqB,EAAe,EAA0B,CAC5D,IAAM,EAAQ,CAAC,KAAK,MAAM,KAAK,EAAI,KAAK,QAAQ,MAAO,CAAK,CAAC,EAAG,EAAE,EAClE,GAAI,KAAK,QAAQ,QAAQ,SAAW,EAElC,OADA,EAAM,KAAK,KAAK,MAAM,GAAG,MAAK,2BAAa,CAAC,EACrC,EAAM,MAAM,EAAG,CAAM,EAG9B,EAAM,KAAK,KAAK,MAAM,GAAG,MAAK,QAAY,CAAC,EAE3C,IAAM,EAAW,KAAK,IAAI,EAAG,EAAS,EAAe,CAAC,EAChD,EAAQ,KAAK,IACjB,EACA,KAAK,IAAI,KAAK,SAAW,EAAW,EAAG,KAAK,IAAI,EAAG,KAAK,QAAQ,QAAQ,OAAS,CAAQ,CAAC,CAC5F,EACA,IAAK,GAAM,CAAC,EAAQ,KAAW,KAAK,QAAQ,QAAQ,MAAM,EAAO,EAAQ,CAAQ,CAAC,CAAC,QAAQ,EAAG,CAC5F,IAAM,EAAU,EAAQ,IAAW,KAAK,SAClC,EAAS,EAAU,KAAK,MAAM,GAAG,SAAA,GAAwB,EAAI,IAC7D,EAAQ,EAAU,KAAK,MAAM,KAAK,CAAM,EAAI,EAC5C,EAAO,IAAW,KAAK,QAAQ,UAAY,KAAK,MAAM,GAAG,MAAK,CAAc,EAAI,GAChF,EAAsB,EAAU,EAAc,gBACpD,EAAM,KAAK,KAAK,MAAM,GAAG,EAAY,EAAa,GAAG,EAAO,GAAG,IAAS,EAAM,CAAK,CAAC,CAAC,CACvF,CACA,OAAO,EAAM,MAAM,EAAG,CAAM,CAC9B,CACF,EAEa,EAAb,cAA4C,CAAY,CAMnC,QACA,KANnB,MAAgB,GAEhB,YACE,EACA,EACA,EACA,EACA,CACA,MAAM,EAAK,CAAK,EAHC,KAAA,QAAA,EACA,KAAA,KAAA,CAGnB,CAEA,YAAY,EAAoB,CAC9B,GAAI,EAAW,EAAM,QAAQ,GAAK,EAAW,EAAM,QAAQ,EAAG,CAC5D,KAAK,KAAK,IAAA,EAAS,EACnB,MACF,CACA,GAAI,EAAW,EAAM,OAAO,EAAG,CAG7B,KAAK,KAAK,KAAK,MAAM,KAAK,GAAK,KAAK,QAAQ,UAAY,EAAE,EAC1D,MACF,CACA,GAAI,EAAW,EAAM,WAAW,EAAG,KAAK,MAAQ,KAAK,MAAM,MAAM,EAAG,EAAE,OACjE,GAAI,IAAS,IAAW,KAAK,MAAQ,QACrC,GAAI,CAAC,EAAe,CAAI,EAAG,KAAK,OAAS,OACzC,OACL,KAAK,IAAI,cAAc,CACzB,CAEA,WAAyC,CACvC,MAAO,CACL,MAAO,EACP,OAAQ,EACR,WAAY,KAAK,QAAQ,WACzB,YAAa,KAAK,QAAQ,SAAW,WAAW,KAAK,QAAQ,WAAa,aAC1E,OAAQ,2DACR,YAAa,CACf,CACF,CAEA,WAAqB,EAAe,EAA0B,CAC5D,IAAM,EAAQ,KAAK,QAAQ,SACvB,EACE,KAAK,MAAM,GAAG,MAAK,CAAW,EAC9B,KAAK,MAAM,GAAG,MAAK,GAAG,EAAe,GAAG,KAAK,QAAQ,UAAU,EAC/D,CACF,EACA,KAAK,MAAM,GAAG,MAAK,CAAW,EAClC,MAAO,CACL,KAAK,MAAM,KAAK,EAAI,KAAK,QAAQ,MAAO,CAAK,CAAC,EAC9C,GACA,EACA,KAAK,MAAM,GAAG,EAAa,EAAI,IAAI,KAAK,SAAwB,CAAK,CAAC,CACxE,CAAC,CAAC,MAAM,EAAG,CAAM,CACnB,CACF,EAEA,eAAsB,EACpB,EACA,EAC6B,CAC7B,OAAO,EAAI,GAAG,QACX,EAAK,EAAO,EAAc,IAAS,IAAI,EAAwB,EAAK,EAAO,EAAS,CAAI,EACzF,CACF,CACF,CAEA,eAAsB,EACpB,EACA,EAC6B,CAC7B,OAAO,EAAI,GAAG,QACX,EAAK,EAAO,EAAc,IAAS,IAAI,EAAuB,EAAK,EAAO,EAAS,CAAI,EACxF,CACF,CACF"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowRunPanel.cjs","names":["DoomOverlay","DoubleEscapeDetector","matchesKey","DOOM_OVERLAY_ACCENT","fit","DOOM_FULLSCREEN_UI_OPTIONS"],"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';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay.ts';\nimport { fit } 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 /** 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 const tail = output.slice(-rows).map((line) => fit(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":"+IAoEA,IAAa,EAAb,cAA+CA,EAAAA,WAAY,CAMtC,QALnB,QAA2B,IAAIC,EAAAA,qBAE/B,YACE,EACA,EACA,EACA,CACA,MAAM,EAAK,CAAK,EAFC,KAAA,QAAA,CAGnB,CASA,YAAY,EAAoB,CAC9B,IAAA,EAAIC,EAAAA,WAAAA,CAAW,EAAM,KAAK,QAAQ,gBAAgB,EAAG,CACnD,KAAK,QAAQ,WAAW,EACxB,MACF,CACA,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,KAAK,QAAQ,aAAa,EAAG,CAChD,KAAK,QAAQ,QAAQ,EACrB,MACF,CAIA,GAAI,KAAK,QAAQ,QAAQ,CAAI,IAAM,QAAS,CAC1C,KAAK,QAAQ,WAAW,EACxB,KAAK,QAAQ,QAAQ,EACrB,MACF,CACA,KAAK,QAAQ,UAAU,CAAI,CAC7B,CAEA,WAAyC,CACvC,MAAO,CACL,MAAO,eACP,OAAQC,EAAAA,oBACR,WAAY,KAAK,QAAQ,WACzB,YAAa,KAAK,QAAQ,YAAc,KAAK,QAAQ,MAAQ,GAAG,KAAK,QAAQ,MAAM,4CACnF,OAAQ,KAAK,QAAQ,OACrB,YAAa,KAAK,QAAQ,MAC1B,YAAa,KAAK,QAAQ,YAAc,yBAA2B,mBACrE,CACF,CAOA,WAAqB,EAAe,EAA0B,CAC5D,GAAM,CAAE,WAAU,UAAW,KAAK,QAAQ,SAAS,CAAK,EAClD,EAAO,EAAS,MAAM,EAAG,CAAM,CAAC,CAAC,IAAK,GAASC,EAAAA,IAAI,EAAM,CAAK,CAAC,EAE/D,EAAQ,EAAS,EAAK,OAC5B,GAAI,GAAS,EAAoB,OAAO,EAAK,MAAM,EAAG,CAAM,EAE5D,IAAM,EAAO,EAAQ,EACf,EAAO,EAAO,MAAM,CAAC,CAAI,CAAC,CAAC,IAAK,GAASA,EAAAA,IAAI,EAAM,CAAK,CAAC,EAC/D,MAAO,CACL,GAAG,EACH,GACA,KAAK,MAAM,GAAG,MAAO,sBAAc,EACnC,GAAI,EAAK,OAAS,EAAI,EAAO,CAAC,KAAK,MAAM,GAAG,MAAO,qBAAO,CAAC,CAC7D,CAAC,CAAC,MAAM,EAAG,CAAM,CACnB,CAEA,SAAgB,CACd,KAAK,QAAQ,UAAU,CACzB,CACF,EAOA,SAAgB,EAAkB,EAA+C,CAC/E,MAAO,CACL,GAAGC,EAAAA,2BACH,eAAgB,CACd,GAAGA,EAAAA,2BAA2B,eAC9B,GAAI,EAAc,CAAC,EAAI,CAAE,aAAc,EAAK,CAC9C,CACF,CACF"}
@@ -0,0 +1 @@
1
+ {"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';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay.ts';\nimport { fit } 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 /** 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 const tail = output.slice(-rows).map((line) => fit(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":"kQAoEA,IAAa,EAAb,cAA+C,CAAY,CAMtC,QALnB,QAA2B,IAAI,EAE/B,YACE,EACA,EACA,EACA,CACA,MAAM,EAAK,CAAK,EAFC,KAAA,QAAA,CAGnB,CASA,YAAY,EAAoB,CAC9B,GAAI,EAAW,EAAM,KAAK,QAAQ,gBAAgB,EAAG,CACnD,KAAK,QAAQ,WAAW,EACxB,MACF,CACA,GAAI,EAAW,EAAM,KAAK,QAAQ,aAAa,EAAG,CAChD,KAAK,QAAQ,QAAQ,EACrB,MACF,CAIA,GAAI,KAAK,QAAQ,QAAQ,CAAI,IAAM,QAAS,CAC1C,KAAK,QAAQ,WAAW,EACxB,KAAK,QAAQ,QAAQ,EACrB,MACF,CACA,KAAK,QAAQ,UAAU,CAAI,CAC7B,CAEA,WAAyC,CACvC,MAAO,CACL,MAAO,eACP,OAAQ,EACR,WAAY,KAAK,QAAQ,WACzB,YAAa,KAAK,QAAQ,YAAc,KAAK,QAAQ,MAAQ,GAAG,KAAK,QAAQ,MAAM,4CACnF,OAAQ,KAAK,QAAQ,OACrB,YAAa,KAAK,QAAQ,MAC1B,YAAa,KAAK,QAAQ,YAAc,yBAA2B,mBACrE,CACF,CAOA,WAAqB,EAAe,EAA0B,CAC5D,GAAM,CAAE,WAAU,UAAW,KAAK,QAAQ,SAAS,CAAK,EAClD,EAAO,EAAS,MAAM,EAAG,CAAM,CAAC,CAAC,IAAK,GAAS,EAAI,EAAM,CAAK,CAAC,EAE/D,EAAQ,EAAS,EAAK,OAC5B,GAAI,GAAS,EAAoB,OAAO,EAAK,MAAM,EAAG,CAAM,EAE5D,IAAM,EAAO,EAAQ,EACf,EAAO,EAAO,MAAM,CAAC,CAAI,CAAC,CAAC,IAAK,GAAS,EAAI,EAAM,CAAK,CAAC,EAC/D,MAAO,CACL,GAAG,EACH,GACA,KAAK,MAAM,GAAG,MAAO,sBAAc,EACnC,GAAI,EAAK,OAAS,EAAI,EAAO,CAAC,KAAK,MAAM,GAAG,MAAO,qBAAO,CAAC,CAC7D,CAAC,CAAC,MAAM,EAAG,CAAM,CACnB,CAEA,SAAgB,CACd,KAAK,QAAQ,UAAU,CACzB,CACF,EAOA,SAAgB,EAAkB,EAA+C,CAC/E,MAAO,CACL,GAAG,EACH,eAAgB,CACd,GAAG,EAA2B,eAC9B,GAAI,EAAc,CAAC,EAAI,CAAE,aAAc,EAAK,CAC9C,CACF,CACF"}
@@ -0,0 +1,2 @@
1
+ function e(e){if(!Number.isFinite(e)||e<0)return`?`;if(e<1e3)return`${e}ms`;let t=Math.round(e/1e3);if(t<60)return`${t}s`;let n=Math.floor(t/60);return n<60?`${n}m${String(t%60).padStart(2,`0`)}s`:`${Math.floor(n/60)}h${String(n%60).padStart(2,`0`)}m`}exports.humanizeDuration=e;
2
+ //# sourceMappingURL=workflowStatusRow.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowStatusRow.cjs","names":[],"sources":["../../../src/tui/workflow/workflowStatusRow.ts"],"sourcesContent":["/**\n * Session reporting for a run's progress log.\n *\n * DESIGN PATTERNS:\n * - Pure derivation. Takes a run's raw progress events and returns report lines,\n * so the behaviour is unit-testable without a registry or a live run.\n *\n * CODING STANDARDS:\n * - Named exports only\n * - Explicit return types on exported members\n *\n * Live progress now renders through `workflowProgressOverlay.ts`. This module\n * remains the compatibility derivation used by terminal summaries and legacy\n * workflow-step cards already persisted in older sessions.\n */\n\nimport type { WorkflowProgressEvent } from '@agimon-ai/workflow-mcp';\n\n/** Step names come from workflow YAML and can be sentences. */\nconst MAX_STEP_LABEL = 48;\n\n/** One session line, plus the key that stops it being reported twice. */\nexport interface StepReport {\n duration?: string;\n job: string;\n key: string;\n status: 'FAILED' | 'FINISHED' | 'STARTED';\n step: string;\n}\n\nfunction truncateStep(step: string): string {\n return step.length <= MAX_STEP_LABEL ? step : `${step.slice(0, MAX_STEP_LABEL - 1)}…`;\n}\n\n/**\n * Human-scale duration.\n *\n * Rounded hard on purpose: this is read at a glance next to a step name, where\n * milliseconds are noise and \"4m12s\" answers the only question being asked.\n */\nexport function humanizeDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return '?';\n if (ms < 1000) return `${ms}ms`;\n const seconds = Math.round(ms / 1000);\n if (seconds < 60) return `${seconds}s`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m${String(seconds % 60).padStart(2, '0')}s`;\n return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;\n}\n\n/**\n * Turn a run's raw progress log into session report lines.\n *\n * Derives durations by pairing each step's start with its end rather than\n * recording a duration in the log, so nothing about the log format has to\n * change and old runs read back the same way.\n *\n * Keyed by the event's own timestamp so a caller polling the same append-only\n * file repeatedly reports each transition exactly once, however often it polls.\n */\nexport function stepReportLines(runKey: string, events: WorkflowProgressEvent[]): StepReport[] {\n const startedAt = new Map<string, string>();\n const reports: StepReport[] = [];\n\n for (const event of events) {\n if (event.type !== 'step' || !event.step) continue;\n const id = `${runKey}/${event.job}`;\n const stepKey = `${id}/${event.step}`;\n const label = truncateStep(event.step);\n\n if (event.status === 'running') {\n startedAt.set(stepKey, event.at);\n reports.push({ job: event.job, key: `started:${stepKey}:${event.at}`, status: 'STARTED', step: label });\n continue;\n }\n\n const began = startedAt.get(stepKey);\n const duration = began ? humanizeDuration(Date.parse(event.at) - Date.parse(began)) : '?';\n // A failed step did finish, but calling it FINISHED buries the one outcome\n // the reader needs to see. Same shape, honest verb.\n const status = event.status === 'failed' ? 'FAILED' : 'FINISHED';\n reports.push({ duration, job: event.job, key: `${status}:${stepKey}:${event.at}`, status, step: label });\n }\n\n return reports;\n}\n"],"mappings":"AAwCA,SAAgB,EAAiB,EAAoB,CACnD,GAAI,CAAC,OAAO,SAAS,CAAE,GAAK,EAAK,EAAG,MAAO,IAC3C,GAAI,EAAK,IAAM,MAAO,GAAG,EAAG,IAC5B,IAAM,EAAU,KAAK,MAAM,EAAK,GAAI,EACpC,GAAI,EAAU,GAAI,MAAO,GAAG,EAAQ,GACpC,IAAM,EAAU,KAAK,MAAM,EAAU,EAAE,EAEvC,OADI,EAAU,GAAW,GAAG,EAAQ,GAAG,OAAO,EAAU,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,EAAE,GACtE,GAAG,KAAK,MAAM,EAAU,EAAE,EAAE,GAAG,OAAO,EAAU,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,EAAE,EAC9E"}
@@ -0,0 +1,2 @@
1
+ function e(e){if(!Number.isFinite(e)||e<0)return`?`;if(e<1e3)return`${e}ms`;let t=Math.round(e/1e3);if(t<60)return`${t}s`;let n=Math.floor(t/60);return n<60?`${n}m${String(t%60).padStart(2,`0`)}s`:`${Math.floor(n/60)}h${String(n%60).padStart(2,`0`)}m`}export{e as humanizeDuration};
2
+ //# sourceMappingURL=workflowStatusRow.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowStatusRow.mjs","names":[],"sources":["../../../src/tui/workflow/workflowStatusRow.ts"],"sourcesContent":["/**\n * Session reporting for a run's progress log.\n *\n * DESIGN PATTERNS:\n * - Pure derivation. Takes a run's raw progress events and returns report lines,\n * so the behaviour is unit-testable without a registry or a live run.\n *\n * CODING STANDARDS:\n * - Named exports only\n * - Explicit return types on exported members\n *\n * Live progress now renders through `workflowProgressOverlay.ts`. This module\n * remains the compatibility derivation used by terminal summaries and legacy\n * workflow-step cards already persisted in older sessions.\n */\n\nimport type { WorkflowProgressEvent } from '@agimon-ai/workflow-mcp';\n\n/** Step names come from workflow YAML and can be sentences. */\nconst MAX_STEP_LABEL = 48;\n\n/** One session line, plus the key that stops it being reported twice. */\nexport interface StepReport {\n duration?: string;\n job: string;\n key: string;\n status: 'FAILED' | 'FINISHED' | 'STARTED';\n step: string;\n}\n\nfunction truncateStep(step: string): string {\n return step.length <= MAX_STEP_LABEL ? step : `${step.slice(0, MAX_STEP_LABEL - 1)}…`;\n}\n\n/**\n * Human-scale duration.\n *\n * Rounded hard on purpose: this is read at a glance next to a step name, where\n * milliseconds are noise and \"4m12s\" answers the only question being asked.\n */\nexport function humanizeDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return '?';\n if (ms < 1000) return `${ms}ms`;\n const seconds = Math.round(ms / 1000);\n if (seconds < 60) return `${seconds}s`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m${String(seconds % 60).padStart(2, '0')}s`;\n return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;\n}\n\n/**\n * Turn a run's raw progress log into session report lines.\n *\n * Derives durations by pairing each step's start with its end rather than\n * recording a duration in the log, so nothing about the log format has to\n * change and old runs read back the same way.\n *\n * Keyed by the event's own timestamp so a caller polling the same append-only\n * file repeatedly reports each transition exactly once, however often it polls.\n */\nexport function stepReportLines(runKey: string, events: WorkflowProgressEvent[]): StepReport[] {\n const startedAt = new Map<string, string>();\n const reports: StepReport[] = [];\n\n for (const event of events) {\n if (event.type !== 'step' || !event.step) continue;\n const id = `${runKey}/${event.job}`;\n const stepKey = `${id}/${event.step}`;\n const label = truncateStep(event.step);\n\n if (event.status === 'running') {\n startedAt.set(stepKey, event.at);\n reports.push({ job: event.job, key: `started:${stepKey}:${event.at}`, status: 'STARTED', step: label });\n continue;\n }\n\n const began = startedAt.get(stepKey);\n const duration = began ? humanizeDuration(Date.parse(event.at) - Date.parse(began)) : '?';\n // A failed step did finish, but calling it FINISHED buries the one outcome\n // the reader needs to see. Same shape, honest verb.\n const status = event.status === 'failed' ? 'FAILED' : 'FINISHED';\n reports.push({ duration, job: event.job, key: `${status}:${stepKey}:${event.at}`, status, step: label });\n }\n\n return reports;\n}\n"],"mappings":"AAwCA,SAAgB,EAAiB,EAAoB,CACnD,GAAI,CAAC,OAAO,SAAS,CAAE,GAAK,EAAK,EAAG,MAAO,IAC3C,GAAI,EAAK,IAAM,MAAO,GAAG,EAAG,IAC5B,IAAM,EAAU,KAAK,MAAM,EAAK,GAAI,EACpC,GAAI,EAAU,GAAI,MAAO,GAAG,EAAQ,GACpC,IAAM,EAAU,KAAK,MAAM,EAAU,EAAE,EAEvC,OADI,EAAU,GAAW,GAAG,EAAQ,GAAG,OAAO,EAAU,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,EAAE,GACtE,GAAG,KAAK,MAAM,EAAU,EAAE,EAAE,GAAG,OAAO,EAAU,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,EAAE,EAC9E"}
@@ -0,0 +1,2 @@
1
+ let e=require("@earendil-works/pi-tui");function t(e){if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e;return typeof t.displayName==`string`&&typeof t.job==`string`&&typeof t.step==`string`&&(t.status===`STARTED`||t.status===`FINISHED`||t.status===`FAILED`)&&(t.duration===void 0||typeof t.duration==`string`)}var n=class{details;theme;constructor(e,t){this.details=e,this.theme=t}render(t){let n=`${this.theme.fg(`accent`,`[${this.details.displayName}]:`)} ${this.details.job}`,r=this.details.duration?`${this.details.status}::${this.details.duration}::${this.details.step}`:`${this.details.status}::${this.details.step}`,i=this.details.status===`FAILED`?`error`:this.details.status===`FINISHED`?`success`:`accent`;return[(0,e.truncateToWidth)(n,t),(0,e.truncateToWidth)(this.theme.fg(i,r),t)]}invalidate(){}};function r(t,r,i,a){let o=new e.Box(i,1,e=>a.bg(`customMessageBg`,e));return o.addChild(t?new n(t,a):new e.Text(r,0,0)),o}exports.isWorkflowStepMessageDetails=t,exports.renderWorkflowStepMessage=r;
2
+ //# sourceMappingURL=workflowStepMessage.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowStepMessage.cjs","names":["truncateToWidth","Box","Text"],"sources":["../../../src/tui/workflow/workflowStepMessage.ts"],"sourcesContent":["import type { Theme } from '@earendil-works/pi-coding-agent';\nimport { Box, type Component, Text, truncateToWidth } from '@earendil-works/pi-tui';\nimport type { StepReport } from './workflowStatusRow';\n\nexport interface WorkflowStepMessageDetails {\n displayName: string;\n duration?: string;\n job: string;\n status: StepReport['status'];\n step: string;\n}\n\nexport function workflowStepMessageDetails(displayName: string, report: StepReport): WorkflowStepMessageDetails {\n return {\n displayName,\n job: report.job,\n status: report.status,\n step: report.step,\n ...(report.duration ? { duration: report.duration } : {}),\n };\n}\n\nexport function formatWorkflowStepMessage(details: WorkflowStepMessageDetails): string {\n const transition = details.duration\n ? `${details.status}::${details.duration}::${details.step}`\n : `${details.status}::${details.step}`;\n return `[${details.displayName}]: ${details.job}\\n${transition}`;\n}\n\nexport function isWorkflowStepMessageDetails(value: unknown): value is WorkflowStepMessageDetails {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const candidate = value as Partial<WorkflowStepMessageDetails>;\n return (\n typeof candidate.displayName === 'string' &&\n typeof candidate.job === 'string' &&\n typeof candidate.step === 'string' &&\n (candidate.status === 'STARTED' || candidate.status === 'FINISHED' || candidate.status === 'FAILED') &&\n (candidate.duration === undefined || typeof candidate.duration === 'string')\n );\n}\n\nclass WorkflowStepMessageLines implements Component {\n constructor(\n private readonly details: WorkflowStepMessageDetails,\n private readonly theme: Theme,\n ) {}\n\n render(width: number): string[] {\n const header = `${this.theme.fg('accent', `[${this.details.displayName}]:`)} ${this.details.job}`;\n const transition = this.details.duration\n ? `${this.details.status}::${this.details.duration}::${this.details.step}`\n : `${this.details.status}::${this.details.step}`;\n const colour =\n this.details.status === 'FAILED' ? 'error' : this.details.status === 'FINISHED' ? 'success' : 'accent';\n return [truncateToWidth(header, width), truncateToWidth(this.theme.fg(colour, transition), width)];\n }\n\n invalidate(): void {}\n}\n\nexport function renderWorkflowStepMessage(\n details: WorkflowStepMessageDetails | undefined,\n fallback: string,\n outputPad: number,\n theme: Theme,\n): Component {\n const box = new Box(outputPad, 1, (text) => theme.bg('customMessageBg', text));\n box.addChild(details ? new WorkflowStepMessageLines(details, theme) : new Text(fallback, 0, 0));\n return box;\n}\n"],"mappings":"wCA6BA,SAAgB,EAA6B,EAAqD,CAChG,GAAI,CAAC,GAAS,OAAO,GAAU,UAAY,MAAM,QAAQ,CAAK,EAAG,MAAO,GACxE,IAAM,EAAY,EAClB,OACE,OAAO,EAAU,aAAgB,UACjC,OAAO,EAAU,KAAQ,UACzB,OAAO,EAAU,MAAS,WACzB,EAAU,SAAW,WAAa,EAAU,SAAW,YAAc,EAAU,SAAW,YAC1F,EAAU,WAAa,IAAA,IAAa,OAAO,EAAU,UAAa,SAEvE,CAEA,IAAM,EAAN,KAAoD,CAE/B,QACA,MAFnB,YACE,EACA,EACA,CAFiB,KAAA,QAAA,EACA,KAAA,MAAA,CAChB,CAEH,OAAO,EAAyB,CAC9B,IAAM,EAAS,GAAG,KAAK,MAAM,GAAG,SAAU,IAAI,KAAK,QAAQ,YAAY,GAAG,EAAE,GAAG,KAAK,QAAQ,MACtF,EAAa,KAAK,QAAQ,SAC5B,GAAG,KAAK,QAAQ,OAAO,IAAI,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,OAClE,GAAG,KAAK,QAAQ,OAAO,IAAI,KAAK,QAAQ,OACtC,EACJ,KAAK,QAAQ,SAAW,SAAW,QAAU,KAAK,QAAQ,SAAW,WAAa,UAAY,SAChG,MAAO,EAAA,EAACA,EAAAA,gBAAAA,CAAgB,EAAQ,CAAK,GAAA,EAAGA,EAAAA,gBAAAA,CAAgB,KAAK,MAAM,GAAG,EAAQ,CAAU,EAAG,CAAK,CAAC,CACnG,CAEA,YAAmB,CAAC,CACtB,EAEA,SAAgB,EACd,EACA,EACA,EACA,EACW,CACX,IAAM,EAAM,IAAIC,EAAAA,IAAI,EAAW,EAAI,GAAS,EAAM,GAAG,kBAAmB,CAAI,CAAC,EAE7E,OADA,EAAI,SAAS,EAAU,IAAI,EAAyB,EAAS,CAAK,EAAI,IAAIC,EAAAA,KAAK,EAAU,EAAG,CAAC,CAAC,EACvF,CACT"}
@@ -0,0 +1,2 @@
1
+ import{Box as e,Text as t,truncateToWidth as n}from"@earendil-works/pi-tui";function r(e){if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e;return typeof t.displayName==`string`&&typeof t.job==`string`&&typeof t.step==`string`&&(t.status===`STARTED`||t.status===`FINISHED`||t.status===`FAILED`)&&(t.duration===void 0||typeof t.duration==`string`)}var i=class{details;theme;constructor(e,t){this.details=e,this.theme=t}render(e){let t=`${this.theme.fg(`accent`,`[${this.details.displayName}]:`)} ${this.details.job}`,r=this.details.duration?`${this.details.status}::${this.details.duration}::${this.details.step}`:`${this.details.status}::${this.details.step}`,i=this.details.status===`FAILED`?`error`:this.details.status===`FINISHED`?`success`:`accent`;return[n(t,e),n(this.theme.fg(i,r),e)]}invalidate(){}};function a(n,r,a,o){let s=new e(a,1,e=>o.bg(`customMessageBg`,e));return s.addChild(n?new i(n,o):new t(r,0,0)),s}export{r as isWorkflowStepMessageDetails,a as renderWorkflowStepMessage};
2
+ //# sourceMappingURL=workflowStepMessage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowStepMessage.mjs","names":[],"sources":["../../../src/tui/workflow/workflowStepMessage.ts"],"sourcesContent":["import type { Theme } from '@earendil-works/pi-coding-agent';\nimport { Box, type Component, Text, truncateToWidth } from '@earendil-works/pi-tui';\nimport type { StepReport } from './workflowStatusRow';\n\nexport interface WorkflowStepMessageDetails {\n displayName: string;\n duration?: string;\n job: string;\n status: StepReport['status'];\n step: string;\n}\n\nexport function workflowStepMessageDetails(displayName: string, report: StepReport): WorkflowStepMessageDetails {\n return {\n displayName,\n job: report.job,\n status: report.status,\n step: report.step,\n ...(report.duration ? { duration: report.duration } : {}),\n };\n}\n\nexport function formatWorkflowStepMessage(details: WorkflowStepMessageDetails): string {\n const transition = details.duration\n ? `${details.status}::${details.duration}::${details.step}`\n : `${details.status}::${details.step}`;\n return `[${details.displayName}]: ${details.job}\\n${transition}`;\n}\n\nexport function isWorkflowStepMessageDetails(value: unknown): value is WorkflowStepMessageDetails {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const candidate = value as Partial<WorkflowStepMessageDetails>;\n return (\n typeof candidate.displayName === 'string' &&\n typeof candidate.job === 'string' &&\n typeof candidate.step === 'string' &&\n (candidate.status === 'STARTED' || candidate.status === 'FINISHED' || candidate.status === 'FAILED') &&\n (candidate.duration === undefined || typeof candidate.duration === 'string')\n );\n}\n\nclass WorkflowStepMessageLines implements Component {\n constructor(\n private readonly details: WorkflowStepMessageDetails,\n private readonly theme: Theme,\n ) {}\n\n render(width: number): string[] {\n const header = `${this.theme.fg('accent', `[${this.details.displayName}]:`)} ${this.details.job}`;\n const transition = this.details.duration\n ? `${this.details.status}::${this.details.duration}::${this.details.step}`\n : `${this.details.status}::${this.details.step}`;\n const colour =\n this.details.status === 'FAILED' ? 'error' : this.details.status === 'FINISHED' ? 'success' : 'accent';\n return [truncateToWidth(header, width), truncateToWidth(this.theme.fg(colour, transition), width)];\n }\n\n invalidate(): void {}\n}\n\nexport function renderWorkflowStepMessage(\n details: WorkflowStepMessageDetails | undefined,\n fallback: string,\n outputPad: number,\n theme: Theme,\n): Component {\n const box = new Box(outputPad, 1, (text) => theme.bg('customMessageBg', text));\n box.addChild(details ? new WorkflowStepMessageLines(details, theme) : new Text(fallback, 0, 0));\n return box;\n}\n"],"mappings":"4EA6BA,SAAgB,EAA6B,EAAqD,CAChG,GAAI,CAAC,GAAS,OAAO,GAAU,UAAY,MAAM,QAAQ,CAAK,EAAG,MAAO,GACxE,IAAM,EAAY,EAClB,OACE,OAAO,EAAU,aAAgB,UACjC,OAAO,EAAU,KAAQ,UACzB,OAAO,EAAU,MAAS,WACzB,EAAU,SAAW,WAAa,EAAU,SAAW,YAAc,EAAU,SAAW,YAC1F,EAAU,WAAa,IAAA,IAAa,OAAO,EAAU,UAAa,SAEvE,CAEA,IAAM,EAAN,KAAoD,CAE/B,QACA,MAFnB,YACE,EACA,EACA,CAFiB,KAAA,QAAA,EACA,KAAA,MAAA,CAChB,CAEH,OAAO,EAAyB,CAC9B,IAAM,EAAS,GAAG,KAAK,MAAM,GAAG,SAAU,IAAI,KAAK,QAAQ,YAAY,GAAG,EAAE,GAAG,KAAK,QAAQ,MACtF,EAAa,KAAK,QAAQ,SAC5B,GAAG,KAAK,QAAQ,OAAO,IAAI,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,OAClE,GAAG,KAAK,QAAQ,OAAO,IAAI,KAAK,QAAQ,OACtC,EACJ,KAAK,QAAQ,SAAW,SAAW,QAAU,KAAK,QAAQ,SAAW,WAAa,UAAY,SAChG,MAAO,CAAC,EAAgB,EAAQ,CAAK,EAAG,EAAgB,KAAK,MAAM,GAAG,EAAQ,CAAU,EAAG,CAAK,CAAC,CACnG,CAEA,YAAmB,CAAC,CACtB,EAEA,SAAgB,EACd,EACA,EACA,EACA,EACW,CACX,IAAM,EAAM,IAAI,EAAI,EAAW,EAAI,GAAS,EAAM,GAAG,kBAAmB,CAAI,CAAC,EAE7E,OADA,EAAI,SAAS,EAAU,IAAI,EAAyB,EAAS,CAAK,EAAI,IAAI,EAAK,EAAU,EAAG,CAAC,CAAC,EACvF,CACT"}