@agimon-ai/doompi-workflow 0.0.1-alpha.73 → 0.0.1-alpha.76

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 (218) hide show
  1. package/LICENSE +4 -2
  2. package/README.md +8 -5
  3. package/package.json +19 -18
  4. package/src/types/webWorkflowTerminal.ts +7 -6
  5. package/dist/_virtual/_rolldown/runtime.cjs +0 -1
  6. package/dist/api-contracts.cjs +0 -1
  7. package/dist/api-contracts.d.cts +0 -2
  8. package/dist/api-contracts.d.mts +0 -2
  9. package/dist/api-contracts.mjs +0 -1
  10. package/dist/extensions/pi.cjs +0 -2
  11. package/dist/extensions/pi.cjs.map +0 -1
  12. package/dist/extensions/pi.d.cts +0 -6
  13. package/dist/extensions/pi.d.cts.map +0 -1
  14. package/dist/extensions/pi.d.mts +0 -6
  15. package/dist/extensions/pi.d.mts.map +0 -1
  16. package/dist/extensions/pi.mjs +0 -2
  17. package/dist/extensions/pi.mjs.map +0 -1
  18. package/dist/extensions/server.cjs +0 -2
  19. package/dist/extensions/server.cjs.map +0 -1
  20. package/dist/extensions/server.d.cts +0 -5
  21. package/dist/extensions/server.d.cts.map +0 -1
  22. package/dist/extensions/server.d.mts +0 -5
  23. package/dist/extensions/server.d.mts.map +0 -1
  24. package/dist/extensions/server.mjs +0 -2
  25. package/dist/extensions/server.mjs.map +0 -1
  26. package/dist/index.cjs +0 -1
  27. package/dist/index.d.cts +0 -2
  28. package/dist/index.d.mts +0 -2
  29. package/dist/index.mjs +0 -1
  30. package/dist/src/constants/workflow.cjs +0 -2
  31. package/dist/src/constants/workflow.cjs.map +0 -1
  32. package/dist/src/constants/workflow.mjs +0 -2
  33. package/dist/src/constants/workflow.mjs.map +0 -1
  34. package/dist/src/controllers/serverRuntime.cjs +0 -2
  35. package/dist/src/controllers/serverRuntime.cjs.map +0 -1
  36. package/dist/src/controllers/serverRuntime.mjs +0 -2
  37. package/dist/src/controllers/serverRuntime.mjs.map +0 -1
  38. package/dist/src/controllers/workflowCatalogChannel.cjs +0 -2
  39. package/dist/src/controllers/workflowCatalogChannel.cjs.map +0 -1
  40. package/dist/src/controllers/workflowCatalogChannel.mjs +0 -2
  41. package/dist/src/controllers/workflowCatalogChannel.mjs.map +0 -1
  42. package/dist/src/controllers/workflowHubApi.cjs +0 -2
  43. package/dist/src/controllers/workflowHubApi.cjs.map +0 -1
  44. package/dist/src/controllers/workflowHubApi.mjs +0 -2
  45. package/dist/src/controllers/workflowHubApi.mjs.map +0 -1
  46. package/dist/src/controllers/workflowsHubChannel.cjs +0 -2
  47. package/dist/src/controllers/workflowsHubChannel.cjs.map +0 -1
  48. package/dist/src/controllers/workflowsHubChannel.mjs +0 -2
  49. package/dist/src/controllers/workflowsHubChannel.mjs.map +0 -1
  50. package/dist/src/schemas/apiContracts.cjs +0 -2
  51. package/dist/src/schemas/apiContracts.cjs.map +0 -1
  52. package/dist/src/schemas/apiContracts.d.cts +0 -53
  53. package/dist/src/schemas/apiContracts.d.cts.map +0 -1
  54. package/dist/src/schemas/apiContracts.d.mts +0 -53
  55. package/dist/src/schemas/apiContracts.d.mts.map +0 -1
  56. package/dist/src/schemas/apiContracts.mjs +0 -2
  57. package/dist/src/schemas/apiContracts.mjs.map +0 -1
  58. package/dist/src/schemas/workflowPi.cjs +0 -2
  59. package/dist/src/schemas/workflowPi.cjs.map +0 -1
  60. package/dist/src/schemas/workflowPi.d.cts +0 -1
  61. package/dist/src/schemas/workflowPi.d.mts +0 -1
  62. package/dist/src/schemas/workflowPi.mjs +0 -2
  63. package/dist/src/schemas/workflowPi.mjs.map +0 -1
  64. package/dist/src/services/backgroundWork/index.cjs +0 -2
  65. package/dist/src/services/backgroundWork/index.cjs.map +0 -1
  66. package/dist/src/services/backgroundWork/index.mjs +0 -2
  67. package/dist/src/services/backgroundWork/index.mjs.map +0 -1
  68. package/dist/src/services/piToolBridge/index.cjs +0 -11
  69. package/dist/src/services/piToolBridge/index.cjs.map +0 -1
  70. package/dist/src/services/piToolBridge/index.mjs +0 -11
  71. package/dist/src/services/piToolBridge/index.mjs.map +0 -1
  72. package/dist/src/services/webWorkflowCatalog/index.cjs +0 -2
  73. package/dist/src/services/webWorkflowCatalog/index.cjs.map +0 -1
  74. package/dist/src/services/webWorkflowCatalog/index.mjs +0 -2
  75. package/dist/src/services/webWorkflowCatalog/index.mjs.map +0 -1
  76. package/dist/src/services/workflowCatalogDeps/index.cjs +0 -2
  77. package/dist/src/services/workflowCatalogDeps/index.cjs.map +0 -1
  78. package/dist/src/services/workflowCatalogDeps/index.mjs +0 -2
  79. package/dist/src/services/workflowCatalogDeps/index.mjs.map +0 -1
  80. package/dist/src/services/workflowExecution/index.cjs +0 -2
  81. package/dist/src/services/workflowExecution/index.cjs.map +0 -1
  82. package/dist/src/services/workflowExecution/index.d.cts +0 -6
  83. package/dist/src/services/workflowExecution/index.d.mts +0 -6
  84. package/dist/src/services/workflowExecution/index.mjs +0 -2
  85. package/dist/src/services/workflowExecution/index.mjs.map +0 -1
  86. package/dist/src/services/workflowFence/index.cjs +0 -2
  87. package/dist/src/services/workflowFence/index.cjs.map +0 -1
  88. package/dist/src/services/workflowFence/index.mjs +0 -2
  89. package/dist/src/services/workflowFence/index.mjs.map +0 -1
  90. package/dist/src/services/workflowLaunchCommand/index.cjs +0 -2
  91. package/dist/src/services/workflowLaunchCommand/index.cjs.map +0 -1
  92. package/dist/src/services/workflowLaunchCommand/index.d.cts +0 -52
  93. package/dist/src/services/workflowLaunchCommand/index.d.cts.map +0 -1
  94. package/dist/src/services/workflowLaunchCommand/index.d.mts +0 -52
  95. package/dist/src/services/workflowLaunchCommand/index.d.mts.map +0 -1
  96. package/dist/src/services/workflowLaunchCommand/index.mjs +0 -2
  97. package/dist/src/services/workflowLaunchCommand/index.mjs.map +0 -1
  98. package/dist/src/services/workflowLauncher/index.cjs +0 -2
  99. package/dist/src/services/workflowLauncher/index.cjs.map +0 -1
  100. package/dist/src/services/workflowLauncher/index.mjs +0 -2
  101. package/dist/src/services/workflowLauncher/index.mjs.map +0 -1
  102. package/dist/src/services/workflowNarration/index.cjs +0 -2
  103. package/dist/src/services/workflowNarration/index.cjs.map +0 -1
  104. package/dist/src/services/workflowNarration/index.mjs +0 -2
  105. package/dist/src/services/workflowNarration/index.mjs.map +0 -1
  106. package/dist/src/services/workflowResource/index.cjs +0 -2
  107. package/dist/src/services/workflowResource/index.cjs.map +0 -1
  108. package/dist/src/services/workflowResource/index.mjs +0 -2
  109. package/dist/src/services/workflowResource/index.mjs.map +0 -1
  110. package/dist/src/services/workflowRuns/index.cjs +0 -3
  111. package/dist/src/services/workflowRuns/index.cjs.map +0 -1
  112. package/dist/src/services/workflowRuns/index.mjs +0 -3
  113. package/dist/src/services/workflowRuns/index.mjs.map +0 -1
  114. package/dist/src/services/workflowTerminal/index.cjs +0 -2
  115. package/dist/src/services/workflowTerminal/index.cjs.map +0 -1
  116. package/dist/src/services/workflowTerminal/index.mjs +0 -2
  117. package/dist/src/services/workflowTerminal/index.mjs.map +0 -1
  118. package/dist/src/services/workflowWatcher/index.cjs +0 -2
  119. package/dist/src/services/workflowWatcher/index.cjs.map +0 -1
  120. package/dist/src/services/workflowWatcher/index.mjs +0 -2
  121. package/dist/src/services/workflowWatcher/index.mjs.map +0 -1
  122. package/dist/src/tools/workflowTools.cjs +0 -5
  123. package/dist/src/tools/workflowTools.cjs.map +0 -1
  124. package/dist/src/tools/workflowTools.mjs +0 -5
  125. package/dist/src/tools/workflowTools.mjs.map +0 -1
  126. package/dist/src/tui/leader.cjs +0 -2
  127. package/dist/src/tui/leader.cjs.map +0 -1
  128. package/dist/src/tui/leader.mjs +0 -2
  129. package/dist/src/tui/leader.mjs.map +0 -1
  130. package/dist/src/tui/workflow/doomOverlay.cjs +0 -2
  131. package/dist/src/tui/workflow/doomOverlay.cjs.map +0 -1
  132. package/dist/src/tui/workflow/doomOverlay.mjs +0 -2
  133. package/dist/src/tui/workflow/doomOverlay.mjs.map +0 -1
  134. package/dist/src/tui/workflow/overlayText.cjs +0 -2
  135. package/dist/src/tui/workflow/overlayText.cjs.map +0 -1
  136. package/dist/src/tui/workflow/overlayText.mjs +0 -2
  137. package/dist/src/tui/workflow/overlayText.mjs.map +0 -1
  138. package/dist/src/tui/workflow/rendering.cjs +0 -2
  139. package/dist/src/tui/workflow/rendering.cjs.map +0 -1
  140. package/dist/src/tui/workflow/rendering.mjs +0 -2
  141. package/dist/src/tui/workflow/rendering.mjs.map +0 -1
  142. package/dist/src/tui/workflow/workflowCatalog.cjs +0 -2
  143. package/dist/src/tui/workflow/workflowCatalog.cjs.map +0 -1
  144. package/dist/src/tui/workflow/workflowCatalog.mjs +0 -2
  145. package/dist/src/tui/workflow/workflowCatalog.mjs.map +0 -1
  146. package/dist/src/tui/workflow/workflowFinishedMessage.cjs +0 -3
  147. package/dist/src/tui/workflow/workflowFinishedMessage.cjs.map +0 -1
  148. package/dist/src/tui/workflow/workflowFinishedMessage.mjs +0 -3
  149. package/dist/src/tui/workflow/workflowFinishedMessage.mjs.map +0 -1
  150. package/dist/src/tui/workflow/workflowInspector.cjs +0 -2
  151. package/dist/src/tui/workflow/workflowInspector.cjs.map +0 -1
  152. package/dist/src/tui/workflow/workflowInspector.mjs +0 -2
  153. package/dist/src/tui/workflow/workflowInspector.mjs.map +0 -1
  154. package/dist/src/tui/workflow/workflowOverlay.cjs +0 -2
  155. package/dist/src/tui/workflow/workflowOverlay.cjs.map +0 -1
  156. package/dist/src/tui/workflow/workflowOverlay.mjs +0 -2
  157. package/dist/src/tui/workflow/workflowOverlay.mjs.map +0 -1
  158. package/dist/src/tui/workflow/workflowPicker.cjs +0 -2
  159. package/dist/src/tui/workflow/workflowPicker.cjs.map +0 -1
  160. package/dist/src/tui/workflow/workflowPicker.mjs +0 -2
  161. package/dist/src/tui/workflow/workflowPicker.mjs.map +0 -1
  162. package/dist/src/tui/workflow/workflowProgressOverlay.cjs +0 -2
  163. package/dist/src/tui/workflow/workflowProgressOverlay.cjs.map +0 -1
  164. package/dist/src/tui/workflow/workflowProgressOverlay.mjs +0 -2
  165. package/dist/src/tui/workflow/workflowProgressOverlay.mjs.map +0 -1
  166. package/dist/src/tui/workflow/workflowPrompt.cjs +0 -2
  167. package/dist/src/tui/workflow/workflowPrompt.cjs.map +0 -1
  168. package/dist/src/tui/workflow/workflowPrompt.mjs +0 -2
  169. package/dist/src/tui/workflow/workflowPrompt.mjs.map +0 -1
  170. package/dist/src/tui/workflow/workflowRunPanel.cjs +0 -2
  171. package/dist/src/tui/workflow/workflowRunPanel.cjs.map +0 -1
  172. package/dist/src/tui/workflow/workflowRunPanel.mjs +0 -2
  173. package/dist/src/tui/workflow/workflowRunPanel.mjs.map +0 -1
  174. package/dist/src/tui/workflow/workflowStatusRow.cjs +0 -2
  175. package/dist/src/tui/workflow/workflowStatusRow.cjs.map +0 -1
  176. package/dist/src/tui/workflow/workflowStatusRow.mjs +0 -2
  177. package/dist/src/tui/workflow/workflowStatusRow.mjs.map +0 -1
  178. package/dist/src/tui/workflow/workflowStepMessage.cjs +0 -2
  179. package/dist/src/tui/workflow/workflowStepMessage.cjs.map +0 -1
  180. package/dist/src/tui/workflow/workflowStepMessage.mjs +0 -2
  181. package/dist/src/tui/workflow/workflowStepMessage.mjs.map +0 -1
  182. package/dist/src/tui/workflow/workflowToolRender.cjs +0 -7
  183. package/dist/src/tui/workflow/workflowToolRender.cjs.map +0 -1
  184. package/dist/src/tui/workflow/workflowToolRender.mjs +0 -7
  185. package/dist/src/tui/workflow/workflowToolRender.mjs.map +0 -1
  186. package/dist/src/tui/workflowRuntime.cjs +0 -9
  187. package/dist/src/tui/workflowRuntime.cjs.map +0 -1
  188. package/dist/src/tui/workflowRuntime.d.cts +0 -33
  189. package/dist/src/tui/workflowRuntime.d.cts.map +0 -1
  190. package/dist/src/tui/workflowRuntime.d.mts +0 -33
  191. package/dist/src/tui/workflowRuntime.d.mts.map +0 -1
  192. package/dist/src/tui/workflowRuntime.mjs +0 -9
  193. package/dist/src/tui/workflowRuntime.mjs.map +0 -1
  194. package/dist/src/types/webWorkflowTerminal.cjs +0 -2
  195. package/dist/src/types/webWorkflowTerminal.cjs.map +0 -1
  196. package/dist/src/types/webWorkflowTerminal.mjs +0 -2
  197. package/dist/src/types/webWorkflowTerminal.mjs.map +0 -1
  198. package/dist/src/types/webWorkflows.cjs +0 -2
  199. package/dist/src/types/webWorkflows.cjs.map +0 -1
  200. package/dist/src/types/webWorkflows.mjs +0 -2
  201. package/dist/src/types/webWorkflows.mjs.map +0 -1
  202. package/src/extensions/web.ts +0 -80
  203. package/src/web/api/terminalApi.ts +0 -208
  204. package/src/web/components/ArtifactsPane.tsx +0 -451
  205. package/src/web/components/LaunchWorkflowDialog.tsx +0 -259
  206. package/src/web/components/StepTerminalPanel.tsx +0 -243
  207. package/src/web/components/WorkflowCatalogDrawer.tsx +0 -229
  208. package/src/web/components/WorkflowToolMessage.tsx +0 -55
  209. package/src/web/components/WorkflowsActivitySection.tsx +0 -155
  210. package/src/web/components/WorkflowsPanel.tsx +0 -750
  211. package/src/web/lib/launchLine.ts +0 -66
  212. package/src/web/lib/runDuration.ts +0 -15
  213. package/src/web/lib/workflowActivity.ts +0 -102
  214. package/src/web/lib/workflowToolRender.ts +0 -585
  215. package/src/web/stores/catalogStore.ts +0 -123
  216. package/src/web/stores/workflowsStore.ts +0 -91
  217. package/src/web/tsconfig.json +0 -22
  218. /package/{src/web/LICENSE → LICENSE.web} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"file":"doomOverlay.mjs","names":[],"sources":["../../../../src/tui/workflow/doomOverlay.ts"],"sourcesContent":["import type { Theme } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\n\nimport { fitLine, frameLine, padLine } from './rendering';\n\nconst DEFAULT_TERMINAL_ROWS = 24;\nconst FULL_CHROME_ROWS = 6;\n/** Two frame columns and two gutter columns, leaving content worth framing. */\nconst MIN_FRAMED_WIDTH = 8;\nconst GUTTER = ' ';\nconst GUTTER_COLUMNS = 2;\n/** Blank columns kept between a row's left cluster and its right-aligned status. */\nconst ROW_GAP = 2;\n/** A secondary segment thinner than this is dropped rather than shredded into a stub. */\nconst MIN_SEGMENT_WIDTH = 8;\nconst ELLIPSIS = '…';\nconst BORDER_COLOR = 'borderMuted';\nconst TEXT_COLOR = 'text';\n/** Filled key caps read as pressable, matching the doom mockups' footers. */\nconst HINT_CAP_BACKGROUND = 'selectedBg';\nconst HINT_SEPARATOR = ' ';\n/** A cap's two pad columns plus the space before its label. */\nconst HINT_CHROME_COLUMNS = 6;\nconst TITLE_COLOR = 'accent';\nconst SECONDARY_COLOR = 'dim';\nconst HORIZONTAL_BORDER = '─';\nconst VERTICAL_BORDER = '│';\nconst TOP_CORNERS = ['╭', '╮'] as const;\nconst DIVIDER_JOINTS = ['├', '┤'] as const;\nconst BOTTOM_CORNERS = ['╰', '╯'] as const;\n\nexport interface DoomOverlayTui {\n terminal?: {\n rows: number;\n columns?: number;\n };\n requestRender(force?: boolean): void;\n}\n\nexport interface DoomOverlayChrome {\n title: string;\n breadcrumb?: string;\n headerRight?: string;\n footer: string;\n footerRight?: string;\n /**\n * Key/label pairs rendered as filled caps, e.g. `[ enter ] edit subject`.\n * Takes precedence over `footer`, which stays for surfaces that want a plain\n * sentence rather than a key legend.\n */\n footerHints?: readonly (readonly [string, string])[];\n /**\n * Signature colour for the surface. When set, the frame takes it and the\n * title renders as a filled badge rather than plain accent text, matching the\n * doom mockups. Left unset, the frame stays muted and the title plain.\n */\n accent?: Parameters<Theme['fg']>[0];\n}\n\n/**\n * One signature colour for every doom overlay: the frame and the title badge.\n * Per-surface colours were tried and read as arbitrary, and they also borrowed\n * tokens that mean something else (`warning`, `syntaxNumber`), so a theme edit\n * elsewhere would have repainted a frame. Change it here to change them all.\n */\nexport const DOOM_OVERLAY_ACCENT = 'mdHeading' satisfies DoomOverlayChrome['accent'];\n\nexport const DOOM_FULLSCREEN_UI_OPTIONS = {\n overlay: true,\n overlayOptions: {\n anchor: 'top-left',\n width: '100%',\n maxHeight: '100%',\n margin: 0,\n },\n} as const;\n\n/**\n * Fits a secondary segment into `budget`, or drops it entirely when too few\n * columns remain for it to stay readable. Truncating everything to fit is what\n * makes a tight header read as one run-on string.\n */\nfunction fitSegment(text: string | undefined, budget: number): string {\n if (!text || budget <= 0) return '';\n return budget < Math.min(visibleWidth(text), MIN_SEGMENT_WIDTH) ? '' : truncateToWidth(text, budget, ELLIPSIS);\n}\n\nexport abstract class DoomOverlay implements Component {\n protected constructor(\n protected readonly tui: DoomOverlayTui,\n protected readonly theme: Theme,\n ) {}\n\n invalidate(): void {\n this.tui.requestRender();\n }\n\n render(width: number): string[] {\n const safeWidth = Math.max(0, Math.floor(width));\n const height = Math.max(0, Math.floor(this.tui.terminal?.rows ?? DEFAULT_TERMINAL_ROWS));\n if (safeWidth === 0 || height === 0) return [];\n\n const chrome = this.getChrome();\n if (height < FULL_CHROME_ROWS || safeWidth < MIN_FRAMED_WIDTH) {\n return this.renderCompact(chrome, safeWidth, height);\n }\n\n const innerWidth = safeWidth - GUTTER_COLUMNS;\n const contentWidth = innerWidth - GUTTER_COLUMNS;\n const bodyHeight = height - FULL_CHROME_ROWS;\n const body = this.renderBody(contentWidth, bodyHeight).slice(0, bodyHeight);\n while (body.length < bodyHeight) body.push('');\n\n const frame = chrome.accent ?? BORDER_COLOR;\n return [\n this.borderRow(TOP_CORNERS, innerWidth, frame),\n this.contentRow(this.headerRow(chrome, contentWidth), contentWidth, safeWidth, frame),\n this.borderRow(DIVIDER_JOINTS, innerWidth, frame, BORDER_COLOR),\n ...body.map((line) => this.contentRow(line, contentWidth, safeWidth, frame)),\n this.borderRow(DIVIDER_JOINTS, innerWidth, frame, BORDER_COLOR),\n this.contentRow(this.footerRow(chrome, contentWidth), contentWidth, safeWidth, frame),\n this.borderRow(BOTTOM_CORNERS, innerWidth, frame),\n ].map((line) => padLine(line, safeWidth));\n }\n\n protected abstract getChrome(): DoomOverlayChrome;\n\n protected abstract renderBody(width: number, height: number): string[];\n\n /**\n * Edge glyphs and the rule between them are coloured separately: the outer\n * rectangle carries the signature colour while an internal separator stays\n * muted, meeting the frame at accent-coloured junctions.\n */\n private borderRow(\n [left, right]: readonly [string, string],\n innerWidth: number,\n edge: DoomOverlayChrome['accent'] = BORDER_COLOR,\n fill: DoomOverlayChrome['accent'] = edge,\n ): string {\n const edgeColour = edge ?? BORDER_COLOR;\n return [\n this.theme.fg(edgeColour, left),\n this.theme.fg(fill ?? edgeColour, HORIZONTAL_BORDER.repeat(innerWidth)),\n this.theme.fg(edgeColour, right),\n ].join('');\n }\n\n private contentRow(\n content: string,\n contentWidth: number,\n width: number,\n colour: DoomOverlayChrome['accent'] = BORDER_COLOR,\n ): string {\n const edge = this.theme.fg(colour ?? BORDER_COLOR, VERTICAL_BORDER);\n return frameLine(`${GUTTER}${padLine(content, contentWidth)}${GUTTER}`, width, edge, edge);\n }\n\n private headerRow(chrome: DoomOverlayChrome, width: number): string {\n // A signature colour turns the title into a filled badge, as the mockups\n // show; without one it stays plain accent text.\n const accent = chrome.accent;\n const titleBudget = accent ? width - GUTTER_COLUMNS : width;\n const title = truncateToWidth(chrome.title, Math.max(0, titleBudget), ELLIPSIS);\n let left = accent\n ? this.theme.inverse(this.theme.bold(this.theme.fg(accent, ` ${title} `)))\n : this.theme.bold(this.theme.fg(TITLE_COLOR, title));\n let leftWidth = visibleWidth(title) + (accent ? GUTTER_COLUMNS : 0);\n\n // The breadcrumb claims room before the right status: it says where you are,\n // which outranks ambient context when the terminal is narrow.\n const breadcrumb = fitSegment(chrome.breadcrumb, width - leftWidth - ROW_GAP);\n if (breadcrumb) {\n left += `${' '.repeat(ROW_GAP)}${this.theme.fg(SECONDARY_COLOR, breadcrumb)}`;\n leftWidth += ROW_GAP + visibleWidth(breadcrumb);\n }\n return this.withRightStatus(left, leftWidth, chrome.headerRight, width);\n }\n\n private footerRow(chrome: DoomOverlayChrome, width: number): string {\n const footer = chrome.footerHints?.length\n ? this.hintLegend(chrome.footerHints, width)\n : this.theme.fg(SECONDARY_COLOR, truncateToWidth(chrome.footer, width, ELLIPSIS));\n return this.withRightStatus(footer, visibleWidth(footer), chrome.footerRight, width);\n }\n\n /**\n * Key legend with each key in a filled cap. Every segment sets its own colour\n * so the caps survive: a cap's reset would otherwise strip the surrounding\n * dim from the labels that follow it.\n */\n private hintLegend(hints: readonly (readonly [string, string])[], width: number): string {\n let legend = '';\n let used = 0;\n for (const [key, label] of hints) {\n const segmentWidth = visibleWidth(key) + visibleWidth(label) + HINT_CHROME_COLUMNS;\n if (used + segmentWidth > width) break;\n const cap = this.theme.bg(HINT_CAP_BACKGROUND, this.theme.fg(TEXT_COLOR, ` ${key} `));\n legend += `${legend ? HINT_SEPARATOR : ''}${cap}${this.theme.fg(SECONDARY_COLOR, ` ${label}`)}`;\n used += segmentWidth;\n }\n return legend;\n }\n\n /**\n * Right-aligns a status cluster, dropping it whole when the row cannot spare\n * the columns. A clipped status reads as a stub (`doom-log…`) that carries\n * less than the blank space it costs.\n */\n private withRightStatus(left: string, leftWidth: number, right: string | undefined, width: number): string {\n const statusWidth = right ? visibleWidth(right) : 0;\n if (!right || statusWidth === 0 || leftWidth + ROW_GAP + statusWidth > width) return left;\n return `${left}${' '.repeat(width - leftWidth - statusWidth)}${this.theme.fg(SECONDARY_COLOR, right)}`;\n }\n\n private renderCompact(chrome: DoomOverlayChrome, width: number, height: number): string[] {\n const lines = Array.from({ length: height }, () => '');\n lines[0] = this.theme.bold(this.theme.fg(TITLE_COLOR, chrome.title));\n if (height > 1) lines[height - 1] = this.theme.fg(SECONDARY_COLOR, chrome.footer);\n return lines.map((line) => padLine(fitLine(line, width), width));\n }\n}\n"],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- let e=require("@earendil-works/pi-tui");const t=`\x1B[0m`;function n(n,r){let i=(0,e.truncateToWidth)(n,Math.max(0,r),`…`).replaceAll(t,``);return i+` `.repeat(Math.max(0,r-(0,e.visibleWidth)(i)))}function r(n,r){let i=Math.max(0,r),a=(0,e.truncateToWidth)(n,i,`…`),o=` `.repeat(Math.max(0,i-(0,e.visibleWidth)(a)));return a.includes(`\x1B`)?`${a}${t}${o}`:`${a}${o}`}function i(t,r,i){let a=(0,e.visibleWidth)(r),o=Math.max(0,i-a-1);return n(t,o)+` `.repeat(Math.max(1,i-o-a))+n(r,a)}function a(t,n){return n<=0?``:(0,e.visibleWidth)(t)<=n?t:`…${t.slice(t.length-Math.max(0,n-1))}`}function o(e){for(let t=0;t<e.length;t++){let n=e.charCodeAt(t);if(n<32||n===127)return!0}return!1}function s(e,t){let n=t.toLowerCase().split(/\s+/).filter(Boolean);if(n.length===0)return!0;let r=e.toLowerCase();return n.every(e=>r.includes(e))}exports.fit=n,exports.fitTerminalLine=r,exports.isControlInput=o,exports.matchesQuery=s,exports.pathTail=a,exports.rightAligned=i;
2
- //# sourceMappingURL=overlayText.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"overlayText.cjs","names":["truncateToWidth","visibleWidth"],"sources":["../../../../src/tui/workflow/overlayText.ts"],"sourcesContent":["/**\n * Text primitives shared by every doom overlay in this extension.\n *\n * Kept in one place because each of them encodes a decision that has to hold on\n * every surface: how a cell is clipped, which end of a path survives, and what\n * counts as a keystroke that carries text.\n */\n\nimport { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\n\nexport const ELLIPSIS = '…';\nexport const CURSOR_BLOCK = '█';\nexport const SELECTION_MARKER = '›';\n/** pi-tui brackets a truncation ellipsis with this; see `fit`. */\nconst HARD_RESET = '\\x1b[0m';\nconst ESCAPE = '\\x1b';\nconst FIRST_PRINTABLE_CODE_POINT = 0x20;\nconst DELETE_CODE_POINT = 0x7f;\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 cell. Nothing here sets a\n * colour that a full reset is the correct end for -- the theme closes its own\n * with `\\x1b[39m` and `\\x1b[49m` -- so the injected resets are dropped.\n */\nexport function 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\n/**\n * Fits a line that carries its own colour, keeping every escape it contains.\n *\n * `fit` above drops hard resets, which is right for a themed row whose\n * background must survive a clipped cell and wrong for captured terminal\n * output: those resets are what END the child's colour runs, and deleting them\n * bleeds a colour through the rest of the panel. This closes the line with a\n * reset instead, before the padding, so no attribute escapes the row.\n */\nexport function fitTerminalLine(text: string, width: number): string {\n const safeWidth = Math.max(0, width);\n const clipped = truncateToWidth(text, safeWidth, ELLIPSIS);\n const padding = ' '.repeat(Math.max(0, safeWidth - visibleWidth(clipped)));\n return clipped.includes(ESCAPE) ? `${clipped}${HARD_RESET}${padding}` : `${clipped}${padding}`;\n}\n\n/** Right-aligns a trailing cluster against a left one, both clipped to fit. */\nexport function 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\n/**\n * Trims from the front rather than the tail.\n *\n * For a path the file name identifies the thing, while the\n * `automations/marketing-workflows/` prefix every entry shares does not.\n */\nexport function pathTail(text: string, width: number): string {\n if (width <= 0) return '';\n if (visibleWidth(text) <= width) return text;\n return `${ELLIPSIS}${text.slice(text.length - Math.max(0, width - 1))}`;\n}\n\n/**\n * Keystrokes that carry no text: escape sequences, control keys, delete.\n *\n * Indexed rather than iterated so surrogate pairs stay intact; both halves sit\n * far above the control range, so no emoji is ever mistaken for one.\n */\nexport function isControlInput(data: string): boolean {\n for (let index = 0; index < data.length; index++) {\n const code = data.charCodeAt(index);\n if (code < FIRST_PRINTABLE_CODE_POINT || code === DELETE_CODE_POINT) return true;\n }\n return false;\n}\n\n/**\n * Whitespace-separated terms, each of which must appear somewhere in the row's\n * searchable text. Splitting on whitespace is what lets `short video` match a\n * row whose words are divided between its title and its file name.\n */\nexport function matchesQuery(haystack: string, query: string): boolean {\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n if (terms.length === 0) return true;\n const lowered = haystack.toLowerCase();\n return terms.every((term) => lowered.includes(term));\n}\n"],"mappings":"wCAcA,MAAM,EAAa,UAanB,SAAgB,EAAI,EAAc,EAAuB,CACvD,IAAM,GAAA,EAAUA,EAAAA,gBAAAA,CAAgB,EAAM,KAAK,IAAI,EAAG,CAAK,EAAA,GAAW,CAAC,CAAC,WAAW,EAAY,EAAE,EAC7F,OAAO,EAAU,IAAI,OAAO,KAAK,IAAI,EAAG,GAAA,EAAQC,EAAAA,aAAAA,CAAa,CAAO,CAAC,CAAC,CACxE,CAWA,SAAgB,EAAgB,EAAc,EAAuB,CACnE,IAAM,EAAY,KAAK,IAAI,EAAG,CAAK,EAC7B,GAAA,EAAUD,EAAAA,gBAAAA,CAAgB,EAAM,EAAA,GAAmB,EACnD,EAAU,IAAI,OAAO,KAAK,IAAI,EAAG,GAAA,EAAYC,EAAAA,aAAAA,CAAa,CAAO,CAAC,CAAC,EACzE,OAAO,EAAQ,SAAS,MAAM,EAAI,GAAG,IAAU,IAAa,IAAY,GAAG,IAAU,GACvF,CAGA,SAAgB,EAAa,EAAc,EAAe,EAAuB,CAC/E,IAAM,GAAA,EAAaA,EAAAA,aAAAA,CAAa,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,CAQA,SAAgB,EAAS,EAAc,EAAuB,CAG5D,OAFI,GAAS,EAAU,IACvB,EAAIA,EAAAA,aAAAA,CAAa,CAAI,GAAK,EAAc,EACjC,IAAc,EAAK,MAAM,EAAK,OAAS,KAAK,IAAI,EAAG,EAAQ,CAAC,CAAC,GACtE,CAQA,SAAgB,EAAe,EAAuB,CACpD,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAK,OAAQ,IAAS,CAChD,IAAM,EAAO,EAAK,WAAW,CAAK,EAClC,GAAI,EAAO,IAA8B,IAAS,IAAmB,MAAO,EAC9E,CACA,MAAO,EACT,CAOA,SAAgB,EAAa,EAAkB,EAAwB,CACrE,IAAM,EAAQ,EAAM,YAAY,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO,EAC7D,GAAI,EAAM,SAAW,EAAG,MAAO,GAC/B,IAAM,EAAU,EAAS,YAAY,EACrC,OAAO,EAAM,MAAO,GAAS,EAAQ,SAAS,CAAI,CAAC,CACrD"}
@@ -1,2 +0,0 @@
1
- import{truncateToWidth as e,visibleWidth as t}from"@earendil-works/pi-tui";const n=`\x1B[0m`;function r(r,i){let a=e(r,Math.max(0,i),`…`).replaceAll(n,``);return a+` `.repeat(Math.max(0,i-t(a)))}function i(r,i){let a=Math.max(0,i),o=e(r,a,`…`),s=` `.repeat(Math.max(0,a-t(o)));return o.includes(`\x1B`)?`${o}${n}${s}`:`${o}${s}`}function a(e,n,i){let a=t(n),o=Math.max(0,i-a-1);return r(e,o)+` `.repeat(Math.max(1,i-o-a))+r(n,a)}function o(e,n){return n<=0?``:t(e)<=n?e:`…${e.slice(e.length-Math.max(0,n-1))}`}function s(e){for(let t=0;t<e.length;t++){let n=e.charCodeAt(t);if(n<32||n===127)return!0}return!1}function c(e,t){let n=t.toLowerCase().split(/\s+/).filter(Boolean);if(n.length===0)return!0;let r=e.toLowerCase();return n.every(e=>r.includes(e))}export{r as fit,i as fitTerminalLine,s as isControlInput,c as matchesQuery,o as pathTail,a as rightAligned};
2
- //# sourceMappingURL=overlayText.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"overlayText.mjs","names":[],"sources":["../../../../src/tui/workflow/overlayText.ts"],"sourcesContent":["/**\n * Text primitives shared by every doom overlay in this extension.\n *\n * Kept in one place because each of them encodes a decision that has to hold on\n * every surface: how a cell is clipped, which end of a path survives, and what\n * counts as a keystroke that carries text.\n */\n\nimport { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\n\nexport const ELLIPSIS = '…';\nexport const CURSOR_BLOCK = '█';\nexport const SELECTION_MARKER = '›';\n/** pi-tui brackets a truncation ellipsis with this; see `fit`. */\nconst HARD_RESET = '\\x1b[0m';\nconst ESCAPE = '\\x1b';\nconst FIRST_PRINTABLE_CODE_POINT = 0x20;\nconst DELETE_CODE_POINT = 0x7f;\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 cell. Nothing here sets a\n * colour that a full reset is the correct end for -- the theme closes its own\n * with `\\x1b[39m` and `\\x1b[49m` -- so the injected resets are dropped.\n */\nexport function 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\n/**\n * Fits a line that carries its own colour, keeping every escape it contains.\n *\n * `fit` above drops hard resets, which is right for a themed row whose\n * background must survive a clipped cell and wrong for captured terminal\n * output: those resets are what END the child's colour runs, and deleting them\n * bleeds a colour through the rest of the panel. This closes the line with a\n * reset instead, before the padding, so no attribute escapes the row.\n */\nexport function fitTerminalLine(text: string, width: number): string {\n const safeWidth = Math.max(0, width);\n const clipped = truncateToWidth(text, safeWidth, ELLIPSIS);\n const padding = ' '.repeat(Math.max(0, safeWidth - visibleWidth(clipped)));\n return clipped.includes(ESCAPE) ? `${clipped}${HARD_RESET}${padding}` : `${clipped}${padding}`;\n}\n\n/** Right-aligns a trailing cluster against a left one, both clipped to fit. */\nexport function 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\n/**\n * Trims from the front rather than the tail.\n *\n * For a path the file name identifies the thing, while the\n * `automations/marketing-workflows/` prefix every entry shares does not.\n */\nexport function pathTail(text: string, width: number): string {\n if (width <= 0) return '';\n if (visibleWidth(text) <= width) return text;\n return `${ELLIPSIS}${text.slice(text.length - Math.max(0, width - 1))}`;\n}\n\n/**\n * Keystrokes that carry no text: escape sequences, control keys, delete.\n *\n * Indexed rather than iterated so surrogate pairs stay intact; both halves sit\n * far above the control range, so no emoji is ever mistaken for one.\n */\nexport function isControlInput(data: string): boolean {\n for (let index = 0; index < data.length; index++) {\n const code = data.charCodeAt(index);\n if (code < FIRST_PRINTABLE_CODE_POINT || code === DELETE_CODE_POINT) return true;\n }\n return false;\n}\n\n/**\n * Whitespace-separated terms, each of which must appear somewhere in the row's\n * searchable text. Splitting on whitespace is what lets `short video` match a\n * row whose words are divided between its title and its file name.\n */\nexport function matchesQuery(haystack: string, query: string): boolean {\n const terms = query.toLowerCase().split(/\\s+/).filter(Boolean);\n if (terms.length === 0) return true;\n const lowered = haystack.toLowerCase();\n return terms.every((term) => lowered.includes(term));\n}\n"],"mappings":"2EAcA,MAAM,EAAa,UAanB,SAAgB,EAAI,EAAc,EAAuB,CACvD,IAAM,EAAU,EAAgB,EAAM,KAAK,IAAI,EAAG,CAAK,EAAA,GAAW,CAAC,CAAC,WAAW,EAAY,EAAE,EAC7F,OAAO,EAAU,IAAI,OAAO,KAAK,IAAI,EAAG,EAAQ,EAAa,CAAO,CAAC,CAAC,CACxE,CAWA,SAAgB,EAAgB,EAAc,EAAuB,CACnE,IAAM,EAAY,KAAK,IAAI,EAAG,CAAK,EAC7B,EAAU,EAAgB,EAAM,EAAA,GAAmB,EACnD,EAAU,IAAI,OAAO,KAAK,IAAI,EAAG,EAAY,EAAa,CAAO,CAAC,CAAC,EACzE,OAAO,EAAQ,SAAS,MAAM,EAAI,GAAG,IAAU,IAAa,IAAY,GAAG,IAAU,GACvF,CAGA,SAAgB,EAAa,EAAc,EAAe,EAAuB,CAC/E,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,CAQA,SAAgB,EAAS,EAAc,EAAuB,CAG5D,OAFI,GAAS,EAAU,GACnB,EAAa,CAAI,GAAK,EAAc,EACjC,IAAc,EAAK,MAAM,EAAK,OAAS,KAAK,IAAI,EAAG,EAAQ,CAAC,CAAC,GACtE,CAQA,SAAgB,EAAe,EAAuB,CACpD,IAAK,IAAI,EAAQ,EAAG,EAAQ,EAAK,OAAQ,IAAS,CAChD,IAAM,EAAO,EAAK,WAAW,CAAK,EAClC,GAAI,EAAO,IAA8B,IAAS,IAAmB,MAAO,EAC9E,CACA,MAAO,EACT,CAOA,SAAgB,EAAa,EAAkB,EAAwB,CACrE,IAAM,EAAQ,EAAM,YAAY,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO,EAC7D,GAAI,EAAM,SAAW,EAAG,MAAO,GAC/B,IAAM,EAAU,EAAS,YAAY,EACrC,OAAO,EAAM,MAAO,GAAS,EAAQ,SAAS,CAAI,CAAC,CACrD"}
@@ -1,2 +0,0 @@
1
- let e=require("@earendil-works/pi-tui");function t(t,n){return(0,e.truncateToWidth)(t,Math.max(0,n),``)}function n(n,r){let i=Math.max(0,r),a=t(n,i);return`${a}${` `.repeat(Math.max(0,i-(0,e.visibleWidth)(a)))}`}function r(r,i,a=`│`,o=`│`){let s=Math.max(0,i),c=(0,e.visibleWidth)(a)+(0,e.visibleWidth)(o);return s<=c?t(`${a}${o}`,s):`${a}${n(r,s-c)}${o}`}exports.fitLine=t,exports.frameLine=r,exports.padLine=n;
2
- //# sourceMappingURL=rendering.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rendering.cjs","names":["truncateToWidth","visibleWidth"],"sources":["../../../../src/tui/workflow/rendering.ts"],"sourcesContent":["import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\n\nconst THOUSAND = 1000;\nconst MILLION = 1_000_000;\n/** Below this many tokens the \"k\" form keeps a decimal, above it rounds. */\nconst K_DECIMAL_LIMIT = 10_000;\n\n/** Compact token count, e.g. `812`, `4.2k`, `186k`, `1.3M`. */\nexport function formatTokens(value: number): string {\n if (value < THOUSAND) return String(value);\n if (value < MILLION) return `${(value / THOUSAND).toFixed(value < K_DECIMAL_LIMIT ? 1 : 0)}k`;\n return `${(value / MILLION).toFixed(1)}M`;\n}\n\nexport function fitLine(text: string, width: number): string {\n return truncateToWidth(text, Math.max(0, width), '');\n}\n\nexport function padLine(text: string, width: number): string {\n const safeWidth = Math.max(0, width);\n const fitted = fitLine(text, safeWidth);\n return `${fitted}${' '.repeat(Math.max(0, safeWidth - visibleWidth(fitted)))}`;\n}\n\nexport function alignLine(left: string, right: string, width: number): string {\n const safeWidth = Math.max(0, width);\n const rightWidth = visibleWidth(right);\n if (!right || rightWidth >= safeWidth) return fitLine(right || left, safeWidth);\n\n const maxLeftWidth = Math.max(0, safeWidth - rightWidth - 1);\n const fittedLeft = fitLine(left, maxLeftWidth);\n const gap = ' '.repeat(Math.max(1, safeWidth - visibleWidth(fittedLeft) - rightWidth));\n return fitLine(`${fittedLeft}${gap}${right}`, safeWidth);\n}\n\nexport function frameLine(content: string, width: number, left = '│', right = '│'): string {\n const safeWidth = Math.max(0, width);\n const frameWidth = visibleWidth(left) + visibleWidth(right);\n if (safeWidth <= frameWidth) return fitLine(`${left}${right}`, safeWidth);\n return `${left}${padLine(content, safeWidth - frameWidth)}${right}`;\n}\n\nexport function packSegments(segments: string[], width: number, separator = ' '): string[] {\n const safeWidth = Math.max(0, width);\n if (safeWidth === 0) return [];\n\n const lines: string[] = [];\n let current = '';\n for (const segment of segments) {\n const candidate = current ? `${current}${separator}${segment}` : segment;\n if (visibleWidth(candidate) <= safeWidth) {\n current = candidate;\n continue;\n }\n if (current) lines.push(fitLine(current, safeWidth));\n current = fitLine(segment, safeWidth);\n }\n if (current) lines.push(current);\n return lines;\n}\n"],"mappings":"wCAcA,SAAgB,EAAQ,EAAc,EAAuB,CAC3D,OAAA,EAAOA,EAAAA,gBAAAA,CAAgB,EAAM,KAAK,IAAI,EAAG,CAAK,EAAG,EAAE,CACrD,CAEA,SAAgB,EAAQ,EAAc,EAAuB,CAC3D,IAAM,EAAY,KAAK,IAAI,EAAG,CAAK,EAC7B,EAAS,EAAQ,EAAM,CAAS,EACtC,MAAO,GAAG,IAAS,IAAI,OAAO,KAAK,IAAI,EAAG,GAAA,EAAYC,EAAAA,aAAAA,CAAa,CAAM,CAAC,CAAC,GAC7E,CAaA,SAAgB,EAAU,EAAiB,EAAe,EAAO,IAAK,EAAQ,IAAa,CACzF,IAAM,EAAY,KAAK,IAAI,EAAG,CAAK,EAC7B,GAAA,EAAaA,EAAAA,aAAAA,CAAa,CAAI,GAAA,EAAIA,EAAAA,aAAAA,CAAa,CAAK,EAE1D,OADI,GAAa,EAAmB,EAAQ,GAAG,IAAO,IAAS,CAAS,EACjE,GAAG,IAAO,EAAQ,EAAS,EAAY,CAAU,IAAI,GAC9D"}
@@ -1,2 +0,0 @@
1
- import{truncateToWidth as e,visibleWidth as t}from"@earendil-works/pi-tui";function n(t,n){return e(t,Math.max(0,n),``)}function r(e,r){let i=Math.max(0,r),a=n(e,i);return`${a}${` `.repeat(Math.max(0,i-t(a)))}`}function i(e,i,a=`│`,o=`│`){let s=Math.max(0,i),c=t(a)+t(o);return s<=c?n(`${a}${o}`,s):`${a}${r(e,s-c)}${o}`}export{n as fitLine,i as frameLine,r as padLine};
2
- //# sourceMappingURL=rendering.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"rendering.mjs","names":[],"sources":["../../../../src/tui/workflow/rendering.ts"],"sourcesContent":["import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui';\n\nconst THOUSAND = 1000;\nconst MILLION = 1_000_000;\n/** Below this many tokens the \"k\" form keeps a decimal, above it rounds. */\nconst K_DECIMAL_LIMIT = 10_000;\n\n/** Compact token count, e.g. `812`, `4.2k`, `186k`, `1.3M`. */\nexport function formatTokens(value: number): string {\n if (value < THOUSAND) return String(value);\n if (value < MILLION) return `${(value / THOUSAND).toFixed(value < K_DECIMAL_LIMIT ? 1 : 0)}k`;\n return `${(value / MILLION).toFixed(1)}M`;\n}\n\nexport function fitLine(text: string, width: number): string {\n return truncateToWidth(text, Math.max(0, width), '');\n}\n\nexport function padLine(text: string, width: number): string {\n const safeWidth = Math.max(0, width);\n const fitted = fitLine(text, safeWidth);\n return `${fitted}${' '.repeat(Math.max(0, safeWidth - visibleWidth(fitted)))}`;\n}\n\nexport function alignLine(left: string, right: string, width: number): string {\n const safeWidth = Math.max(0, width);\n const rightWidth = visibleWidth(right);\n if (!right || rightWidth >= safeWidth) return fitLine(right || left, safeWidth);\n\n const maxLeftWidth = Math.max(0, safeWidth - rightWidth - 1);\n const fittedLeft = fitLine(left, maxLeftWidth);\n const gap = ' '.repeat(Math.max(1, safeWidth - visibleWidth(fittedLeft) - rightWidth));\n return fitLine(`${fittedLeft}${gap}${right}`, safeWidth);\n}\n\nexport function frameLine(content: string, width: number, left = '│', right = '│'): string {\n const safeWidth = Math.max(0, width);\n const frameWidth = visibleWidth(left) + visibleWidth(right);\n if (safeWidth <= frameWidth) return fitLine(`${left}${right}`, safeWidth);\n return `${left}${padLine(content, safeWidth - frameWidth)}${right}`;\n}\n\nexport function packSegments(segments: string[], width: number, separator = ' '): string[] {\n const safeWidth = Math.max(0, width);\n if (safeWidth === 0) return [];\n\n const lines: string[] = [];\n let current = '';\n for (const segment of segments) {\n const candidate = current ? `${current}${separator}${segment}` : segment;\n if (visibleWidth(candidate) <= safeWidth) {\n current = candidate;\n continue;\n }\n if (current) lines.push(fitLine(current, safeWidth));\n current = fitLine(segment, safeWidth);\n }\n if (current) lines.push(current);\n return lines;\n}\n"],"mappings":"2EAcA,SAAgB,EAAQ,EAAc,EAAuB,CAC3D,OAAO,EAAgB,EAAM,KAAK,IAAI,EAAG,CAAK,EAAG,EAAE,CACrD,CAEA,SAAgB,EAAQ,EAAc,EAAuB,CAC3D,IAAM,EAAY,KAAK,IAAI,EAAG,CAAK,EAC7B,EAAS,EAAQ,EAAM,CAAS,EACtC,MAAO,GAAG,IAAS,IAAI,OAAO,KAAK,IAAI,EAAG,EAAY,EAAa,CAAM,CAAC,CAAC,GAC7E,CAaA,SAAgB,EAAU,EAAiB,EAAe,EAAO,IAAK,EAAQ,IAAa,CACzF,IAAM,EAAY,KAAK,IAAI,EAAG,CAAK,EAC7B,EAAa,EAAa,CAAI,EAAI,EAAa,CAAK,EAE1D,OADI,GAAa,EAAmB,EAAQ,GAAG,IAAO,IAAS,CAAS,EACjE,GAAG,IAAO,EAAQ,EAAS,EAAY,CAAU,IAAI,GAC9D"}
@@ -1,2 +0,0 @@
1
- const e=require("./doomOverlay.cjs"),t=require("./overlayText.cjs");let n=require("@earendil-works/pi-tui");e.DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;const r=`(unnamed)`,i=`No workflow definitions were found in this repository.`,a=[`triggers`,`inputs`,`steps`],o={t:`triggers`,i:`inputs`,s:`steps`},s=[[`↑↓`,`move`],[`JK`,`scroll`],[`tab`,`cycle`],[`/`,`filter`],[`r`,`launch`]],c=[[`enter`,`keep`],[`esc`,`clear`],[`ctrl+u`,`wipe`]];function l(e){let t=Math.max(2,e-(0,n.visibleWidth)(`│`)),r=Math.max(1,Math.floor(t/3)),i=Math.max(1,t-r),a=+(r>=3&&i>=3);return{leftContentWidth:Math.max(1,r-a),rightContentWidth:Math.max(1,i-a),gutterWidth:a}}function u(e,t,r,i,a){for(let o of(0,n.wrapTextWithAnsi)(t,Math.max(1,r)))e.push(a.fg(i,o))}function d(e,t,n,r,i){if(e.push(i.bold(i.fg(`accent`,t))),n.length===0)e.push(i.fg(`dim`,` none`));else for(let t of n)u(e,` • ${t}`,r,`text`,i);e.push(``)}function f(e){let t=[e.required?`required`:`optional`,e.default===void 0?void 0:`default ${e.default}`,e.options&&e.options.length>0?`one of ${e.options.join(`, `)}`:void 0].filter(e=>!!e),n=e.description?`${e.description} · `:``;return`${e.name} — ${n}${t.join(` · `)}`}function p(e,t,n,r){let i=[];if(e.error)return i.push(r.bold(r.fg(`error`,`WORKFLOW ERROR`))),u(i,e.error,n,`error`,r),i.push(``),i;if(t===`triggers`)return d(i,`TRIGGERS`,e.triggers,n,r),d(i,`RUNNERS`,e.runners??[`any available runner`],n,r),i;if(t===`inputs`)return d(i,`DISPATCH INPUTS`,e.inputs.map(f),n,r),i;for(let t of e.jobs){if(i.push(r.bold(r.fg(`accent`,t.name))),t.steps.length===0)i.push(r.fg(`dim`,` no steps`));else for(let e of t.steps)u(i,` • ${e}`,n,`text`,r);i.push(``)}return e.jobs.length===0&&d(i,`JOBS`,[],n,r),i}function m(e,t,r){let i=a.map(t=>{let n=t.toUpperCase();return t===e?r.inverse(r.bold(` ${n} `)):r.fg(`dim`,` ${n} `)}).join(` `);return(0,n.truncateToWidth)(i,Math.max(0,t),`…`)}function h(e,n){return n.trim()?e.filter(e=>t.matchesQuery([e.name,e.relativePath,e.description,e.tags.join(` `)].join(` `),n)):e}var g=class extends e.DoomOverlay{done;options;rows;detailCache=new Map;cursorIndex=0;tab=`triggers`;query=``;filtering=!1;notice;listPageSize=1;detailPageSize=1;detailOffset=0;detailTotal=0;constructor(e,t,n,r,i){super(e,t),this.done=r,this.options=i,this.rows=[...n].sort((e,t)=>e.name.localeCompare(t.name))}matches(){return h(this.rows,this.query)}cursorRow(){let e=this.matches();return e[Math.min(this.cursorIndex,Math.max(0,e.length-1))]}detailFor(e){let t=this.detailCache.get(e.key);if(t)return t;let n=this.options.loadDetail(e);return this.detailCache.set(e.key,n),n}moveCursor(e){let t=this.matches().length;if(t===0)return;let n=Math.max(0,Math.min(t-1,this.cursorIndex+e));n!==this.cursorIndex&&(this.cursorIndex=n,this.detailOffset=0,this.invalidate())}switchTab(e){this.tab!==e&&(this.tab=e,this.detailOffset=0,this.invalidate())}cycleTab(){let e=a.indexOf(this.tab);this.switchTab(a[(e+1)%a.length]??`triggers`)}scrollDetail(e){if(!this.cursorRow())return;let t=Math.max(0,this.detailTotal-this.detailPageSize),n=Math.max(0,Math.min(t,this.detailOffset+e));n!==this.detailOffset&&(this.detailOffset=n,this.invalidate())}setNotice(e){this.notice=e,this.invalidate()}setQuery(e){e!==this.query&&(this.query=e,this.cursorIndex=0,this.detailOffset=0,this.invalidate())}launchCursor(){let e=this.cursorRow();if(!e){this.setNotice(`launch unavailable · no workflow is selected`);return}let t=this.options.launchWorkflow;if(!t){this.setNotice(`launch unavailable · no workflow launcher is attached`);return}let n=this.detailFor(e);if(n.error){this.setNotice(`launch unavailable · ${n.error}`);return}this.done(void 0),t(e)}handleFilterInput(e){if((0,n.matchesKey)(e,`escape`)||(0,n.matchesKey)(e,`ctrl+c`)){this.filtering=!1,this.setQuery(``),this.invalidate();return}if((0,n.matchesKey)(e,`enter`)){this.filtering=!1,this.invalidate();return}if((0,n.matchesKey)(e,`backspace`)){this.setQuery(this.query.slice(0,-1));return}if(e===``){this.setQuery(``);return}t.isControlInput(e)||this.setQuery(this.query+e)}handleInput(e){if(this.filtering){this.handleFilterInput(e);return}if((0,n.matchesKey)(e,`escape`)||(0,n.matchesKey)(e,`ctrl+c`)){this.done(void 0);return}if((0,n.matchesKey)(e,n.Key.shift(`k`)))return this.scrollDetail(-1);if((0,n.matchesKey)(e,n.Key.shift(`j`)))return this.scrollDetail(1);if((0,n.matchesKey)(e,`up`)||e===`k`)return this.moveCursor(-1);if((0,n.matchesKey)(e,`down`)||e===`j`)return this.moveCursor(1);if((0,n.matchesKey)(e,`pageUp`))return this.moveCursor(-this.listPageSize);if((0,n.matchesKey)(e,`pageDown`))return this.moveCursor(this.listPageSize);if((0,n.matchesKey)(e,`tab`))return this.cycleTab();if(e===`/`){this.filtering=!0,this.notice=void 0,this.invalidate();return}if(e===`r`)return this.launchCursor();let t=o[e];t&&this.switchTab(t)}getChrome(){let t=this.matches(),n=this.query?`${t.length} of ${this.rows.length}`:`${this.rows.length}`;return{title:`WORKFLOWS`,accent:e.DOOM_OVERLAY_ACCENT,breadcrumb:`SPC › w / workflows › l / list`,headerRight:`${n} workflows · ${this.tab}`,footer:this.filtering?`enter keep · esc clear · ctrl+u wipe`:`↑↓ cursor · JK scroll · tab detail · / filter · r launch · esc close`,footerHints:this.filtering?c:s,footerRight:t.length?`${Math.min(this.cursorIndex,t.length-1)+1}/${t.length}`:`no match`}}renderBody(e,n){let r=this.filtering||this.query?1:0,i=r+(this.notice===void 0?0:1),a=Math.max(0,n-i),o=l(e),s=this.renderList(o.leftContentWidth,a),c=this.renderDetail(o.rightContentWidth,a),u=` `.repeat(o.gutterWidth),d=this.theme.fg(`borderMuted`,`│`),f=[];for(let e=0;e<a;e++)f.push(`${t.fit(s[e]??``,o.leftContentWidth)}${u}${d}${u}${t.fit(c[e]??``,o.rightContentWidth)}`);if(r){let e=this.filtering?`${this.query}█`:this.query;f.push(` ${this.theme.bold(this.theme.fg(`accent`,`FILTER`))} ${e}`)}return this.notice!==void 0&&f.push(` ${this.theme.fg(`muted`,this.notice)}`),f.slice(0,n).map(n=>t.fit(n,e))}renderList(e,n){let a=this.matches(),o=[this.theme.bold(this.theme.fg(`accent`,`WORKFLOWS ${a.length}`))];if(a.length===0)return u(o,this.rows.length===0?i:`No workflow matches this filter.`,e,`dim`,this.theme),this.listPageSize=1,o.slice(0,n);this.listPageSize=Math.max(1,Math.floor((n-1)/2));let s=Math.min(this.cursorIndex,a.length-1),c=Math.max(0,Math.min(s-this.listPageSize+1,Math.max(0,a.length-this.listPageSize)));for(let[n,i]of a.slice(c,c+this.listPageSize).entries()){let a=c+n===s,l=a?this.theme.fg(`accent`,`›`):` `,u=i.name||r,d=a?this.theme.bold(this.theme.fg(`accent`,u)):this.theme.fg(`text`,u),f=this.theme.fg(`dim`,t.pathTail(i.relativePath,Math.max(0,e-2)));o.push(t.fit(`${l} ${d}`,e)),o.push(t.fit(` ${f}`,e))}return o.slice(0,n)}renderDetail(e,n){let a=this.cursorRow();if(!a){this.detailTotal=0,this.detailPageSize=Math.max(1,n),this.detailOffset=0;let t=[this.theme.bold(this.theme.fg(`accent`,`WORKFLOW DETAIL`)),``];return u(t,this.rows.length===0?i:`No workflow matches this filter.`,e,`text`,this.theme),t.slice(0,n)}let o=[t.rightAligned(this.theme.bold(this.theme.fg(`accent`,`INSPECTING ${a.name||r}`)),this.theme.fg(`dim`,t.pathTail(a.relativePath,Math.max(0,Math.floor(e/2)))),e)];a.description&&u(o,a.description,e,`text`,this.theme),a.tags.length>0&&u(o,a.tags.join(` · `),e,`dim`,this.theme),o.push(m(this.tab,e,this.theme),``);let s=p(this.detailFor(a),this.tab,e,this.theme);return this.detailPageSize=Math.max(0,n-o.length),this.detailTotal=s.length,this.detailOffset=Math.min(this.detailOffset,Math.max(0,s.length-this.detailPageSize)),[...o,...s.slice(this.detailOffset,this.detailOffset+this.detailPageSize)].slice(0,n)}};async function _(t,n,r){await t.ui.custom((e,t,i,a)=>new g(e,t,n,a,r),e.DOOM_FULLSCREEN_UI_OPTIONS)}exports.WorkflowCatalogComponent=g,exports.filterWorkflowRows=h,exports.openWorkflowCatalogOverlay=_,exports.workflowDetailLines=p;
2
- //# sourceMappingURL=workflowCatalog.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"workflowCatalog.cjs","names":["DOOM_FULLSCREEN_UI_OPTIONS","visibleWidth","wrapTextWithAnsi","truncateToWidth","matchesQuery","DoomOverlay","matchesKey","isControlInput","Key","DOOM_OVERLAY_ACCENT","fit","pathTail","rightAligned"],"sources":["../../../../src/tui/workflow/workflowCatalog.ts"],"sourcesContent":["/**\n * The `SPC w l` board: every workflow this repository defines, with the cursor\n * workflow's parsed detail beside it and `r` to launch it.\n *\n * WHY NOT `workflowPicker.ts`:\n * That surface answers \"which one\", hands a value back, and closes - which is\n * what `SPC w c` recovery still needs. This one answers \"what is this workflow,\n * and do I want to run it\", so the detail has to be on screen next to the list\n * rather than behind a selection. The picker stays for the pick-and-return case\n * rather than growing a second mode.\n *\n * DESIGN PATTERNS:\n * - Detail is loaded through an injected loader, so parsing a workflow file\n * stays out of the TUI and the surface is assertable as text. Loaded on\n * demand for the cursor row and cached by row key: a repository with fifty\n * workflows must not parse fifty files to draw one pane\n * - A row is two lines, name over path. One line made the name - the field a\n * reader picks a row by - share a third of the overlay with a path that\n * repeats its directory on every row\n * - `/` opens the filter rather than plain letters filtering as they are typed:\n * the letter keys carry commands here (`r` launch, `t`/`i`/`s` tabs)\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay';\nimport {\n CURSOR_BLOCK,\n ELLIPSIS,\n fit,\n isControlInput,\n matchesQuery,\n pathTail,\n rightAligned,\n SELECTION_MARKER,\n} from './overlayText';\n\nexport const WORKFLOW_CATALOG_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\n/** One workflow file, as the catalog tool reports it. */\nexport interface WorkflowCatalogRow {\n /** Stable identity and the loader's cache key: the absolute path. */\n key: string;\n name: string;\n relativePath: string;\n description: string;\n tags: readonly string[];\n}\n\nexport interface WorkflowInputSummary {\n name: string;\n description?: string;\n required?: boolean;\n default?: string;\n options?: readonly string[];\n}\n\nexport interface WorkflowJobSummary {\n name: string;\n steps: readonly string[];\n}\n\n/**\n * What the detail pane shows for one workflow. `runners` is undefined when the\n * workflow names no runner map at all, which is not the same as naming an empty\n * one - see `compatibleRunners`.\n */\nexport interface WorkflowCatalogDetail {\n triggers: readonly string[];\n inputs: readonly WorkflowInputSummary[];\n jobs: readonly WorkflowJobSummary[];\n runners?: readonly string[];\n /** Set when the file could not be parsed; the pane shows this instead of guessing. */\n error?: string;\n}\n\nexport type WorkflowDetailLoader = (row: WorkflowCatalogRow) => WorkflowCatalogDetail;\n\n/** Starts one workflow and returns immediately; the overlay closes on launch. */\nexport type WorkflowLaunchDispatcher = (row: WorkflowCatalogRow) => void;\n\nexport interface WorkflowCatalogOptions {\n loadDetail: WorkflowDetailLoader;\n /** Absent until a composition root wires the launch key to a real launcher. */\n launchWorkflow?: WorkflowLaunchDispatcher;\n}\n\nexport type WorkflowDetailTab = 'triggers' | 'inputs' | 'steps';\n\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\nconst TITLE = 'WORKFLOWS';\nconst BREADCRUMB = 'SPC › w / workflows › l / list';\nconst DIVIDER = '│';\nconst DEFAULT_PAGE_SIZE = 1;\nconst ROWS_PER_ENTRY = 2;\n/** Indent under `${SELECTION_MARKER} `, so the path line hangs under the name. */\nconst META_INDENT = ' ';\nconst EMPTY_NAME = '(unnamed)';\nconst LAUNCH_KEY = 'r';\nconst FILTER_KEY = '/';\nconst KEY_CLEAR = '\\x15';\nconst FILTER_LABEL = 'FILTER';\nconst EMPTY_MESSAGE = 'No workflow definitions were found in this repository.';\nconst TAB_ORDER: readonly WorkflowDetailTab[] = ['triggers', 'inputs', 'steps'];\nconst TAB_KEYS: Record<string, WorkflowDetailTab> = { t: 'triggers', i: 'inputs', s: 'steps' };\nconst FOOTER = '↑↓ cursor · JK scroll · tab detail · / filter · r launch · esc close';\nconst FOOTER_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'move'],\n ['JK', 'scroll'],\n ['tab', 'cycle'],\n ['/', 'filter'],\n ['r', 'launch'],\n];\nconst FILTER_FOOTER = 'enter keep · esc clear · ctrl+u wipe';\nconst FILTER_FOOTER_HINTS: readonly (readonly [string, string])[] = [\n ['enter', 'keep'],\n ['esc', 'clear'],\n ['ctrl+u', 'wipe'],\n];\n\ninterface SplitLayout {\n leftContentWidth: number;\n rightContentWidth: number;\n gutterWidth: number;\n}\n\nfunction splitLayout(width: number): SplitLayout {\n const paneBudget = Math.max(2, width - visibleWidth(DIVIDER));\n const leftPaneWidth = Math.max(1, Math.floor(paneBudget / 3));\n const rightPaneWidth = Math.max(1, paneBudget - leftPaneWidth);\n const gutterWidth = leftPaneWidth >= 3 && rightPaneWidth >= 3 ? 1 : 0;\n return {\n leftContentWidth: Math.max(1, leftPaneWidth - gutterWidth),\n rightContentWidth: Math.max(1, rightPaneWidth - gutterWidth),\n gutterWidth,\n };\n}\n\nfunction appendWrapped(lines: string[], text: string, width: number, colour: ThemeColor, theme: Theme): void {\n for (const line of wrapTextWithAnsi(text, Math.max(1, width))) lines.push(theme.fg(colour, line));\n}\n\nfunction appendSection(lines: string[], heading: string, items: readonly string[], width: number, theme: Theme): void {\n lines.push(theme.bold(theme.fg('accent', heading)));\n if (items.length === 0) lines.push(theme.fg('dim', ' none'));\n else for (const item of items) appendWrapped(lines, ` • ${item}`, width, 'text', theme);\n lines.push('');\n}\n\nfunction inputLabel(input: WorkflowInputSummary): string {\n const qualifiers = [\n input.required ? 'required' : 'optional',\n input.default === undefined ? undefined : `default ${input.default}`,\n input.options && input.options.length > 0 ? `one of ${input.options.join(', ')}` : undefined,\n ].filter((part): part is string => Boolean(part));\n const description = input.description ? `${input.description} · ` : '';\n return `${input.name} — ${description}${qualifiers.join(' · ')}`;\n}\n\nexport function workflowDetailLines(\n detail: WorkflowCatalogDetail,\n tab: WorkflowDetailTab,\n width: number,\n theme: Theme,\n): string[] {\n const lines: string[] = [];\n if (detail.error) {\n lines.push(theme.bold(theme.fg('error', 'WORKFLOW ERROR')));\n appendWrapped(lines, detail.error, width, 'error', theme);\n lines.push('');\n return lines;\n }\n if (tab === 'triggers') {\n appendSection(lines, 'TRIGGERS', detail.triggers, width, theme);\n // No runner map means every runner qualifies, which a bare \"none\" would\n // report as the opposite.\n appendSection(lines, 'RUNNERS', detail.runners ?? ['any available runner'], width, theme);\n return lines;\n }\n if (tab === 'inputs') {\n appendSection(lines, 'DISPATCH INPUTS', detail.inputs.map(inputLabel), width, theme);\n return lines;\n }\n for (const job of detail.jobs) {\n lines.push(theme.bold(theme.fg('accent', job.name)));\n if (job.steps.length === 0) lines.push(theme.fg('dim', ' no steps'));\n else for (const step of job.steps) appendWrapped(lines, ` • ${step}`, width, 'text', theme);\n lines.push('');\n }\n if (detail.jobs.length === 0) appendSection(lines, 'JOBS', [], width, theme);\n return lines;\n}\n\nfunction tabStrip(tab: WorkflowDetailTab, width: number, theme: Theme): string {\n const labels = TAB_ORDER.map((candidate) => {\n const label = candidate.toUpperCase();\n return candidate === tab ? theme.inverse(theme.bold(` ${label} `)) : theme.fg('dim', ` ${label} `);\n }).join(' ');\n return truncateToWidth(labels, Math.max(0, width), ELLIPSIS);\n}\n\nexport function filterWorkflowRows(rows: readonly WorkflowCatalogRow[], query: string): readonly WorkflowCatalogRow[] {\n if (!query.trim()) return rows;\n return rows.filter((row) =>\n matchesQuery([row.name, row.relativePath, row.description, row.tags.join(' ')].join(' '), query),\n );\n}\n\nexport class WorkflowCatalogComponent extends DoomOverlay {\n private readonly rows: readonly WorkflowCatalogRow[];\n private readonly detailCache = new Map<string, WorkflowCatalogDetail>();\n private cursorIndex = 0;\n private tab: WorkflowDetailTab = 'triggers';\n private query = '';\n private filtering = false;\n private notice: string | undefined;\n private listPageSize = DEFAULT_PAGE_SIZE;\n private detailPageSize = DEFAULT_PAGE_SIZE;\n private detailOffset = 0;\n private detailTotal = 0;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n rows: readonly WorkflowCatalogRow[],\n private readonly done: (result: undefined) => void,\n private readonly options: WorkflowCatalogOptions,\n ) {\n super(tui, theme);\n this.rows = [...rows].sort((left, right) => left.name.localeCompare(right.name));\n }\n\n private matches(): readonly WorkflowCatalogRow[] {\n return filterWorkflowRows(this.rows, this.query);\n }\n\n private cursorRow(): WorkflowCatalogRow | undefined {\n const matches = this.matches();\n return matches[Math.min(this.cursorIndex, Math.max(0, matches.length - 1))];\n }\n\n /** Parsed once per workflow, then reused for every repaint of that row. */\n private detailFor(row: WorkflowCatalogRow): WorkflowCatalogDetail {\n const cached = this.detailCache.get(row.key);\n if (cached) return cached;\n const detail = this.options.loadDetail(row);\n this.detailCache.set(row.key, detail);\n return detail;\n }\n\n private moveCursor(delta: number): void {\n const count = this.matches().length;\n if (count === 0) return;\n const next = Math.max(0, Math.min(count - 1, this.cursorIndex + delta));\n if (next === this.cursorIndex) return;\n this.cursorIndex = next;\n // The detail pane follows the cursor, so its scroll belonged to the row\n // that was showing, not the one replacing it.\n this.detailOffset = 0;\n this.invalidate();\n }\n\n private switchTab(tab: WorkflowDetailTab): void {\n if (this.tab === tab) return;\n this.tab = tab;\n this.detailOffset = 0;\n this.invalidate();\n }\n\n private cycleTab(): void {\n const current = TAB_ORDER.indexOf(this.tab);\n this.switchTab(TAB_ORDER[(current + 1) % TAB_ORDER.length] ?? 'triggers');\n }\n\n private scrollDetail(delta: number): void {\n if (!this.cursorRow()) return;\n const maximum = Math.max(0, this.detailTotal - this.detailPageSize);\n const next = Math.max(0, Math.min(maximum, this.detailOffset + delta));\n if (next === this.detailOffset) return;\n this.detailOffset = next;\n this.invalidate();\n }\n\n private setNotice(notice: string): void {\n this.notice = notice;\n this.invalidate();\n }\n\n private setQuery(query: string): void {\n if (query === this.query) return;\n this.query = query;\n // The previous index named a row this filter may no longer list.\n this.cursorIndex = 0;\n this.detailOffset = 0;\n this.invalidate();\n }\n\n private launchCursor(): void {\n const row = this.cursorRow();\n if (!row) {\n this.setNotice('launch unavailable · no workflow is selected');\n return;\n }\n const launchWorkflow = this.options.launchWorkflow;\n if (!launchWorkflow) {\n this.setNotice('launch unavailable · no workflow launcher is attached');\n return;\n }\n const detail = this.detailFor(row);\n if (detail.error) {\n this.setNotice(`launch unavailable · ${detail.error}`);\n return;\n }\n // Closing first is what makes the launcher's own prompts reachable: they\n // are host dialogs, and a fullscreen overlay sits over them.\n this.done(undefined);\n launchWorkflow(row);\n }\n\n private handleFilterInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.filtering = false;\n this.setQuery('');\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'enter')) {\n this.filtering = false;\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'backspace')) {\n this.setQuery(this.query.slice(0, -1));\n return;\n }\n if (data === KEY_CLEAR) {\n this.setQuery('');\n return;\n }\n if (!isControlInput(data)) this.setQuery(this.query + data);\n }\n\n handleInput(data: string): void {\n if (this.filtering) {\n this.handleFilterInput(data);\n return;\n }\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, Key.shift('k'))) return this.scrollDetail(-1);\n if (matchesKey(data, Key.shift('j'))) return this.scrollDetail(1);\n if (matchesKey(data, 'up') || data === 'k') return this.moveCursor(-1);\n if (matchesKey(data, 'down') || data === 'j') return this.moveCursor(1);\n if (matchesKey(data, 'pageUp')) return this.moveCursor(-this.listPageSize);\n if (matchesKey(data, 'pageDown')) return this.moveCursor(this.listPageSize);\n if (matchesKey(data, 'tab')) return this.cycleTab();\n if (data === FILTER_KEY) {\n this.filtering = true;\n this.notice = undefined;\n this.invalidate();\n return;\n }\n if (data === LAUNCH_KEY) return this.launchCursor();\n const tab = TAB_KEYS[data];\n if (tab) this.switchTab(tab);\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: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${scope} workflows · ${this.tab}`,\n footer: this.filtering ? FILTER_FOOTER : FOOTER,\n footerHints: this.filtering ? FILTER_FOOTER_HINTS : FOOTER_HINTS,\n footerRight: matches.length\n ? `${Math.min(this.cursorIndex, matches.length - 1) + 1}/${matches.length}`\n : 'no match',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const filterRows = this.filtering || this.query ? 1 : 0;\n const transientRows = filterRows + (this.notice === undefined ? 0 : 1);\n const mainHeight = Math.max(0, height - transientRows);\n const layout = splitLayout(width);\n const left = this.renderList(layout.leftContentWidth, mainHeight);\n const right = this.renderDetail(layout.rightContentWidth, mainHeight);\n const gutter = ' '.repeat(layout.gutterWidth);\n const divider = this.theme.fg('borderMuted', DIVIDER);\n const lines: string[] = [];\n for (let index = 0; index < mainHeight; index++) {\n lines.push(\n `${fit(left[index] ?? '', layout.leftContentWidth)}${gutter}${divider}${gutter}${fit(\n right[index] ?? '',\n layout.rightContentWidth,\n )}`,\n );\n }\n if (filterRows) {\n const value = this.filtering ? `${this.query}${CURSOR_BLOCK}` : this.query;\n lines.push(` ${this.theme.bold(this.theme.fg('accent', FILTER_LABEL))} ${value}`);\n }\n if (this.notice !== undefined) lines.push(` ${this.theme.fg('muted', this.notice)}`);\n return lines.slice(0, height).map((line) => fit(line, width));\n }\n\n private renderList(width: number, height: number): string[] {\n const matches = this.matches();\n const lines = [this.theme.bold(this.theme.fg('accent', `WORKFLOWS ${matches.length}`))];\n if (matches.length === 0) {\n appendWrapped(\n lines,\n this.rows.length === 0 ? EMPTY_MESSAGE : 'No workflow matches this filter.',\n width,\n 'dim',\n this.theme,\n );\n this.listPageSize = DEFAULT_PAGE_SIZE;\n return lines.slice(0, height);\n }\n\n this.listPageSize = Math.max(DEFAULT_PAGE_SIZE, Math.floor((height - 1) / ROWS_PER_ENTRY));\n const active = Math.min(this.cursorIndex, matches.length - 1);\n const start = Math.max(\n 0,\n Math.min(active - this.listPageSize + 1, Math.max(0, matches.length - this.listPageSize)),\n );\n for (const [offset, row] of matches.slice(start, start + this.listPageSize).entries()) {\n const current = start + offset === active;\n const marker = current ? this.theme.fg('accent', SELECTION_MARKER) : ' ';\n const label = row.name || EMPTY_NAME;\n const name = current ? this.theme.bold(this.theme.fg('accent', label)) : this.theme.fg('text', label);\n const meta = this.theme.fg('dim', pathTail(row.relativePath, Math.max(0, width - META_INDENT.length)));\n lines.push(fit(`${marker} ${name}`, width));\n lines.push(fit(`${META_INDENT}${meta}`, width));\n }\n return lines.slice(0, height);\n }\n\n private renderDetail(width: number, height: number): string[] {\n const row = this.cursorRow();\n if (!row) {\n this.detailTotal = 0;\n this.detailPageSize = Math.max(DEFAULT_PAGE_SIZE, height);\n this.detailOffset = 0;\n const lines = [this.theme.bold(this.theme.fg('accent', 'WORKFLOW DETAIL')), ''];\n appendWrapped(\n lines,\n this.rows.length === 0 ? EMPTY_MESSAGE : 'No workflow matches this filter.',\n width,\n 'text',\n this.theme,\n );\n return lines.slice(0, height);\n }\n\n const header = [\n rightAligned(\n this.theme.bold(this.theme.fg('accent', `INSPECTING ${row.name || EMPTY_NAME}`)),\n this.theme.fg('dim', pathTail(row.relativePath, Math.max(0, Math.floor(width / 2)))),\n width,\n ),\n ];\n if (row.description) appendWrapped(header, row.description, width, 'text', this.theme);\n if (row.tags.length > 0) appendWrapped(header, row.tags.join(' · '), width, 'dim', this.theme);\n header.push(tabStrip(this.tab, width, this.theme), '');\n\n const body = workflowDetailLines(this.detailFor(row), this.tab, width, this.theme);\n this.detailPageSize = Math.max(0, height - header.length);\n this.detailTotal = body.length;\n this.detailOffset = Math.min(this.detailOffset, Math.max(0, body.length - this.detailPageSize));\n return [...header, ...body.slice(this.detailOffset, this.detailOffset + this.detailPageSize)].slice(0, height);\n }\n}\n\nexport async function openWorkflowCatalogOverlay(\n ctx: ExtensionContext,\n rows: readonly WorkflowCatalogRow[],\n options: WorkflowCatalogOptions,\n): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new WorkflowCatalogComponent(tui, theme, rows, done, options),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
@@ -1,2 +0,0 @@
1
- import{DOOM_FULLSCREEN_UI_OPTIONS as e,DOOM_OVERLAY_ACCENT as t,DoomOverlay as n}from"./doomOverlay.mjs";import{fit as r,isControlInput as i,matchesQuery as a,pathTail as o,rightAligned as s}from"./overlayText.mjs";import{Key as c,matchesKey as l,truncateToWidth as u,visibleWidth as d,wrapTextWithAnsi as f}from"@earendil-works/pi-tui";e.overlayOptions;const p=`(unnamed)`,m=`No workflow definitions were found in this repository.`,h=[`triggers`,`inputs`,`steps`],g={t:`triggers`,i:`inputs`,s:`steps`},_=[[`↑↓`,`move`],[`JK`,`scroll`],[`tab`,`cycle`],[`/`,`filter`],[`r`,`launch`]],v=[[`enter`,`keep`],[`esc`,`clear`],[`ctrl+u`,`wipe`]];function y(e){let t=Math.max(2,e-d(`│`)),n=Math.max(1,Math.floor(t/3)),r=Math.max(1,t-n),i=+(n>=3&&r>=3);return{leftContentWidth:Math.max(1,n-i),rightContentWidth:Math.max(1,r-i),gutterWidth:i}}function b(e,t,n,r,i){for(let a of f(t,Math.max(1,n)))e.push(i.fg(r,a))}function x(e,t,n,r,i){if(e.push(i.bold(i.fg(`accent`,t))),n.length===0)e.push(i.fg(`dim`,` none`));else for(let t of n)b(e,` • ${t}`,r,`text`,i);e.push(``)}function S(e){let t=[e.required?`required`:`optional`,e.default===void 0?void 0:`default ${e.default}`,e.options&&e.options.length>0?`one of ${e.options.join(`, `)}`:void 0].filter(e=>!!e),n=e.description?`${e.description} · `:``;return`${e.name} — ${n}${t.join(` · `)}`}function C(e,t,n,r){let i=[];if(e.error)return i.push(r.bold(r.fg(`error`,`WORKFLOW ERROR`))),b(i,e.error,n,`error`,r),i.push(``),i;if(t===`triggers`)return x(i,`TRIGGERS`,e.triggers,n,r),x(i,`RUNNERS`,e.runners??[`any available runner`],n,r),i;if(t===`inputs`)return x(i,`DISPATCH INPUTS`,e.inputs.map(S),n,r),i;for(let t of e.jobs){if(i.push(r.bold(r.fg(`accent`,t.name))),t.steps.length===0)i.push(r.fg(`dim`,` no steps`));else for(let e of t.steps)b(i,` • ${e}`,n,`text`,r);i.push(``)}return e.jobs.length===0&&x(i,`JOBS`,[],n,r),i}function w(e,t,n){let r=h.map(t=>{let r=t.toUpperCase();return t===e?n.inverse(n.bold(` ${r} `)):n.fg(`dim`,` ${r} `)}).join(` `);return u(r,Math.max(0,t),`…`)}function T(e,t){return t.trim()?e.filter(e=>a([e.name,e.relativePath,e.description,e.tags.join(` `)].join(` `),t)):e}var E=class extends n{done;options;rows;detailCache=new Map;cursorIndex=0;tab=`triggers`;query=``;filtering=!1;notice;listPageSize=1;detailPageSize=1;detailOffset=0;detailTotal=0;constructor(e,t,n,r,i){super(e,t),this.done=r,this.options=i,this.rows=[...n].sort((e,t)=>e.name.localeCompare(t.name))}matches(){return T(this.rows,this.query)}cursorRow(){let e=this.matches();return e[Math.min(this.cursorIndex,Math.max(0,e.length-1))]}detailFor(e){let t=this.detailCache.get(e.key);if(t)return t;let n=this.options.loadDetail(e);return this.detailCache.set(e.key,n),n}moveCursor(e){let t=this.matches().length;if(t===0)return;let n=Math.max(0,Math.min(t-1,this.cursorIndex+e));n!==this.cursorIndex&&(this.cursorIndex=n,this.detailOffset=0,this.invalidate())}switchTab(e){this.tab!==e&&(this.tab=e,this.detailOffset=0,this.invalidate())}cycleTab(){let e=h.indexOf(this.tab);this.switchTab(h[(e+1)%h.length]??`triggers`)}scrollDetail(e){if(!this.cursorRow())return;let t=Math.max(0,this.detailTotal-this.detailPageSize),n=Math.max(0,Math.min(t,this.detailOffset+e));n!==this.detailOffset&&(this.detailOffset=n,this.invalidate())}setNotice(e){this.notice=e,this.invalidate()}setQuery(e){e!==this.query&&(this.query=e,this.cursorIndex=0,this.detailOffset=0,this.invalidate())}launchCursor(){let e=this.cursorRow();if(!e){this.setNotice(`launch unavailable · no workflow is selected`);return}let t=this.options.launchWorkflow;if(!t){this.setNotice(`launch unavailable · no workflow launcher is attached`);return}let n=this.detailFor(e);if(n.error){this.setNotice(`launch unavailable · ${n.error}`);return}this.done(void 0),t(e)}handleFilterInput(e){if(l(e,`escape`)||l(e,`ctrl+c`)){this.filtering=!1,this.setQuery(``),this.invalidate();return}if(l(e,`enter`)){this.filtering=!1,this.invalidate();return}if(l(e,`backspace`)){this.setQuery(this.query.slice(0,-1));return}if(e===``){this.setQuery(``);return}i(e)||this.setQuery(this.query+e)}handleInput(e){if(this.filtering){this.handleFilterInput(e);return}if(l(e,`escape`)||l(e,`ctrl+c`)){this.done(void 0);return}if(l(e,c.shift(`k`)))return this.scrollDetail(-1);if(l(e,c.shift(`j`)))return this.scrollDetail(1);if(l(e,`up`)||e===`k`)return this.moveCursor(-1);if(l(e,`down`)||e===`j`)return this.moveCursor(1);if(l(e,`pageUp`))return this.moveCursor(-this.listPageSize);if(l(e,`pageDown`))return this.moveCursor(this.listPageSize);if(l(e,`tab`))return this.cycleTab();if(e===`/`){this.filtering=!0,this.notice=void 0,this.invalidate();return}if(e===`r`)return this.launchCursor();let t=g[e];t&&this.switchTab(t)}getChrome(){let e=this.matches(),n=this.query?`${e.length} of ${this.rows.length}`:`${this.rows.length}`;return{title:`WORKFLOWS`,accent:t,breadcrumb:`SPC › w / workflows › l / list`,headerRight:`${n} workflows · ${this.tab}`,footer:this.filtering?`enter keep · esc clear · ctrl+u wipe`:`↑↓ cursor · JK scroll · tab detail · / filter · r launch · esc close`,footerHints:this.filtering?v:_,footerRight:e.length?`${Math.min(this.cursorIndex,e.length-1)+1}/${e.length}`:`no match`}}renderBody(e,t){let n=this.filtering||this.query?1:0,i=n+(this.notice===void 0?0:1),a=Math.max(0,t-i),o=y(e),s=this.renderList(o.leftContentWidth,a),c=this.renderDetail(o.rightContentWidth,a),l=` `.repeat(o.gutterWidth),u=this.theme.fg(`borderMuted`,`│`),d=[];for(let e=0;e<a;e++)d.push(`${r(s[e]??``,o.leftContentWidth)}${l}${u}${l}${r(c[e]??``,o.rightContentWidth)}`);if(n){let e=this.filtering?`${this.query}█`:this.query;d.push(` ${this.theme.bold(this.theme.fg(`accent`,`FILTER`))} ${e}`)}return this.notice!==void 0&&d.push(` ${this.theme.fg(`muted`,this.notice)}`),d.slice(0,t).map(t=>r(t,e))}renderList(e,t){let n=this.matches(),i=[this.theme.bold(this.theme.fg(`accent`,`WORKFLOWS ${n.length}`))];if(n.length===0)return b(i,this.rows.length===0?m:`No workflow matches this filter.`,e,`dim`,this.theme),this.listPageSize=1,i.slice(0,t);this.listPageSize=Math.max(1,Math.floor((t-1)/2));let a=Math.min(this.cursorIndex,n.length-1),s=Math.max(0,Math.min(a-this.listPageSize+1,Math.max(0,n.length-this.listPageSize)));for(let[t,c]of n.slice(s,s+this.listPageSize).entries()){let n=s+t===a,l=n?this.theme.fg(`accent`,`›`):` `,u=c.name||p,d=n?this.theme.bold(this.theme.fg(`accent`,u)):this.theme.fg(`text`,u),f=this.theme.fg(`dim`,o(c.relativePath,Math.max(0,e-2)));i.push(r(`${l} ${d}`,e)),i.push(r(` ${f}`,e))}return i.slice(0,t)}renderDetail(e,t){let n=this.cursorRow();if(!n){this.detailTotal=0,this.detailPageSize=Math.max(1,t),this.detailOffset=0;let n=[this.theme.bold(this.theme.fg(`accent`,`WORKFLOW DETAIL`)),``];return b(n,this.rows.length===0?m:`No workflow matches this filter.`,e,`text`,this.theme),n.slice(0,t)}let r=[s(this.theme.bold(this.theme.fg(`accent`,`INSPECTING ${n.name||p}`)),this.theme.fg(`dim`,o(n.relativePath,Math.max(0,Math.floor(e/2)))),e)];n.description&&b(r,n.description,e,`text`,this.theme),n.tags.length>0&&b(r,n.tags.join(` · `),e,`dim`,this.theme),r.push(w(this.tab,e,this.theme),``);let i=C(this.detailFor(n),this.tab,e,this.theme);return this.detailPageSize=Math.max(0,t-r.length),this.detailTotal=i.length,this.detailOffset=Math.min(this.detailOffset,Math.max(0,i.length-this.detailPageSize)),[...r,...i.slice(this.detailOffset,this.detailOffset+this.detailPageSize)].slice(0,t)}};async function D(t,n,r){await t.ui.custom((e,t,i,a)=>new E(e,t,n,a,r),e)}export{E as WorkflowCatalogComponent,T as filterWorkflowRows,D as openWorkflowCatalogOverlay,C as workflowDetailLines};
2
- //# sourceMappingURL=workflowCatalog.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"workflowCatalog.mjs","names":[],"sources":["../../../../src/tui/workflow/workflowCatalog.ts"],"sourcesContent":["/**\n * The `SPC w l` board: every workflow this repository defines, with the cursor\n * workflow's parsed detail beside it and `r` to launch it.\n *\n * WHY NOT `workflowPicker.ts`:\n * That surface answers \"which one\", hands a value back, and closes - which is\n * what `SPC w c` recovery still needs. This one answers \"what is this workflow,\n * and do I want to run it\", so the detail has to be on screen next to the list\n * rather than behind a selection. The picker stays for the pick-and-return case\n * rather than growing a second mode.\n *\n * DESIGN PATTERNS:\n * - Detail is loaded through an injected loader, so parsing a workflow file\n * stays out of the TUI and the surface is assertable as text. Loaded on\n * demand for the cursor row and cached by row key: a repository with fifty\n * workflows must not parse fifty files to draw one pane\n * - A row is two lines, name over path. One line made the name - the field a\n * reader picks a row by - share a third of the overlay with a path that\n * repeats its directory on every row\n * - `/` opens the filter rather than plain letters filtering as they are typed:\n * the letter keys carry commands here (`r` launch, `t`/`i`/`s` tabs)\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport { Key, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay';\nimport {\n CURSOR_BLOCK,\n ELLIPSIS,\n fit,\n isControlInput,\n matchesQuery,\n pathTail,\n rightAligned,\n SELECTION_MARKER,\n} from './overlayText';\n\nexport const WORKFLOW_CATALOG_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\n/** One workflow file, as the catalog tool reports it. */\nexport interface WorkflowCatalogRow {\n /** Stable identity and the loader's cache key: the absolute path. */\n key: string;\n name: string;\n relativePath: string;\n description: string;\n tags: readonly string[];\n}\n\nexport interface WorkflowInputSummary {\n name: string;\n description?: string;\n required?: boolean;\n default?: string;\n options?: readonly string[];\n}\n\nexport interface WorkflowJobSummary {\n name: string;\n steps: readonly string[];\n}\n\n/**\n * What the detail pane shows for one workflow. `runners` is undefined when the\n * workflow names no runner map at all, which is not the same as naming an empty\n * one - see `compatibleRunners`.\n */\nexport interface WorkflowCatalogDetail {\n triggers: readonly string[];\n inputs: readonly WorkflowInputSummary[];\n jobs: readonly WorkflowJobSummary[];\n runners?: readonly string[];\n /** Set when the file could not be parsed; the pane shows this instead of guessing. */\n error?: string;\n}\n\nexport type WorkflowDetailLoader = (row: WorkflowCatalogRow) => WorkflowCatalogDetail;\n\n/** Starts one workflow and returns immediately; the overlay closes on launch. */\nexport type WorkflowLaunchDispatcher = (row: WorkflowCatalogRow) => void;\n\nexport interface WorkflowCatalogOptions {\n loadDetail: WorkflowDetailLoader;\n /** Absent until a composition root wires the launch key to a real launcher. */\n launchWorkflow?: WorkflowLaunchDispatcher;\n}\n\nexport type WorkflowDetailTab = 'triggers' | 'inputs' | 'steps';\n\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\nconst TITLE = 'WORKFLOWS';\nconst BREADCRUMB = 'SPC › w / workflows › l / list';\nconst DIVIDER = '│';\nconst DEFAULT_PAGE_SIZE = 1;\nconst ROWS_PER_ENTRY = 2;\n/** Indent under `${SELECTION_MARKER} `, so the path line hangs under the name. */\nconst META_INDENT = ' ';\nconst EMPTY_NAME = '(unnamed)';\nconst LAUNCH_KEY = 'r';\nconst FILTER_KEY = '/';\nconst KEY_CLEAR = '\\x15';\nconst FILTER_LABEL = 'FILTER';\nconst EMPTY_MESSAGE = 'No workflow definitions were found in this repository.';\nconst TAB_ORDER: readonly WorkflowDetailTab[] = ['triggers', 'inputs', 'steps'];\nconst TAB_KEYS: Record<string, WorkflowDetailTab> = { t: 'triggers', i: 'inputs', s: 'steps' };\nconst FOOTER = '↑↓ cursor · JK scroll · tab detail · / filter · r launch · esc close';\nconst FOOTER_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'move'],\n ['JK', 'scroll'],\n ['tab', 'cycle'],\n ['/', 'filter'],\n ['r', 'launch'],\n];\nconst FILTER_FOOTER = 'enter keep · esc clear · ctrl+u wipe';\nconst FILTER_FOOTER_HINTS: readonly (readonly [string, string])[] = [\n ['enter', 'keep'],\n ['esc', 'clear'],\n ['ctrl+u', 'wipe'],\n];\n\ninterface SplitLayout {\n leftContentWidth: number;\n rightContentWidth: number;\n gutterWidth: number;\n}\n\nfunction splitLayout(width: number): SplitLayout {\n const paneBudget = Math.max(2, width - visibleWidth(DIVIDER));\n const leftPaneWidth = Math.max(1, Math.floor(paneBudget / 3));\n const rightPaneWidth = Math.max(1, paneBudget - leftPaneWidth);\n const gutterWidth = leftPaneWidth >= 3 && rightPaneWidth >= 3 ? 1 : 0;\n return {\n leftContentWidth: Math.max(1, leftPaneWidth - gutterWidth),\n rightContentWidth: Math.max(1, rightPaneWidth - gutterWidth),\n gutterWidth,\n };\n}\n\nfunction appendWrapped(lines: string[], text: string, width: number, colour: ThemeColor, theme: Theme): void {\n for (const line of wrapTextWithAnsi(text, Math.max(1, width))) lines.push(theme.fg(colour, line));\n}\n\nfunction appendSection(lines: string[], heading: string, items: readonly string[], width: number, theme: Theme): void {\n lines.push(theme.bold(theme.fg('accent', heading)));\n if (items.length === 0) lines.push(theme.fg('dim', ' none'));\n else for (const item of items) appendWrapped(lines, ` • ${item}`, width, 'text', theme);\n lines.push('');\n}\n\nfunction inputLabel(input: WorkflowInputSummary): string {\n const qualifiers = [\n input.required ? 'required' : 'optional',\n input.default === undefined ? undefined : `default ${input.default}`,\n input.options && input.options.length > 0 ? `one of ${input.options.join(', ')}` : undefined,\n ].filter((part): part is string => Boolean(part));\n const description = input.description ? `${input.description} · ` : '';\n return `${input.name} — ${description}${qualifiers.join(' · ')}`;\n}\n\nexport function workflowDetailLines(\n detail: WorkflowCatalogDetail,\n tab: WorkflowDetailTab,\n width: number,\n theme: Theme,\n): string[] {\n const lines: string[] = [];\n if (detail.error) {\n lines.push(theme.bold(theme.fg('error', 'WORKFLOW ERROR')));\n appendWrapped(lines, detail.error, width, 'error', theme);\n lines.push('');\n return lines;\n }\n if (tab === 'triggers') {\n appendSection(lines, 'TRIGGERS', detail.triggers, width, theme);\n // No runner map means every runner qualifies, which a bare \"none\" would\n // report as the opposite.\n appendSection(lines, 'RUNNERS', detail.runners ?? ['any available runner'], width, theme);\n return lines;\n }\n if (tab === 'inputs') {\n appendSection(lines, 'DISPATCH INPUTS', detail.inputs.map(inputLabel), width, theme);\n return lines;\n }\n for (const job of detail.jobs) {\n lines.push(theme.bold(theme.fg('accent', job.name)));\n if (job.steps.length === 0) lines.push(theme.fg('dim', ' no steps'));\n else for (const step of job.steps) appendWrapped(lines, ` • ${step}`, width, 'text', theme);\n lines.push('');\n }\n if (detail.jobs.length === 0) appendSection(lines, 'JOBS', [], width, theme);\n return lines;\n}\n\nfunction tabStrip(tab: WorkflowDetailTab, width: number, theme: Theme): string {\n const labels = TAB_ORDER.map((candidate) => {\n const label = candidate.toUpperCase();\n return candidate === tab ? theme.inverse(theme.bold(` ${label} `)) : theme.fg('dim', ` ${label} `);\n }).join(' ');\n return truncateToWidth(labels, Math.max(0, width), ELLIPSIS);\n}\n\nexport function filterWorkflowRows(rows: readonly WorkflowCatalogRow[], query: string): readonly WorkflowCatalogRow[] {\n if (!query.trim()) return rows;\n return rows.filter((row) =>\n matchesQuery([row.name, row.relativePath, row.description, row.tags.join(' ')].join(' '), query),\n );\n}\n\nexport class WorkflowCatalogComponent extends DoomOverlay {\n private readonly rows: readonly WorkflowCatalogRow[];\n private readonly detailCache = new Map<string, WorkflowCatalogDetail>();\n private cursorIndex = 0;\n private tab: WorkflowDetailTab = 'triggers';\n private query = '';\n private filtering = false;\n private notice: string | undefined;\n private listPageSize = DEFAULT_PAGE_SIZE;\n private detailPageSize = DEFAULT_PAGE_SIZE;\n private detailOffset = 0;\n private detailTotal = 0;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n rows: readonly WorkflowCatalogRow[],\n private readonly done: (result: undefined) => void,\n private readonly options: WorkflowCatalogOptions,\n ) {\n super(tui, theme);\n this.rows = [...rows].sort((left, right) => left.name.localeCompare(right.name));\n }\n\n private matches(): readonly WorkflowCatalogRow[] {\n return filterWorkflowRows(this.rows, this.query);\n }\n\n private cursorRow(): WorkflowCatalogRow | undefined {\n const matches = this.matches();\n return matches[Math.min(this.cursorIndex, Math.max(0, matches.length - 1))];\n }\n\n /** Parsed once per workflow, then reused for every repaint of that row. */\n private detailFor(row: WorkflowCatalogRow): WorkflowCatalogDetail {\n const cached = this.detailCache.get(row.key);\n if (cached) return cached;\n const detail = this.options.loadDetail(row);\n this.detailCache.set(row.key, detail);\n return detail;\n }\n\n private moveCursor(delta: number): void {\n const count = this.matches().length;\n if (count === 0) return;\n const next = Math.max(0, Math.min(count - 1, this.cursorIndex + delta));\n if (next === this.cursorIndex) return;\n this.cursorIndex = next;\n // The detail pane follows the cursor, so its scroll belonged to the row\n // that was showing, not the one replacing it.\n this.detailOffset = 0;\n this.invalidate();\n }\n\n private switchTab(tab: WorkflowDetailTab): void {\n if (this.tab === tab) return;\n this.tab = tab;\n this.detailOffset = 0;\n this.invalidate();\n }\n\n private cycleTab(): void {\n const current = TAB_ORDER.indexOf(this.tab);\n this.switchTab(TAB_ORDER[(current + 1) % TAB_ORDER.length] ?? 'triggers');\n }\n\n private scrollDetail(delta: number): void {\n if (!this.cursorRow()) return;\n const maximum = Math.max(0, this.detailTotal - this.detailPageSize);\n const next = Math.max(0, Math.min(maximum, this.detailOffset + delta));\n if (next === this.detailOffset) return;\n this.detailOffset = next;\n this.invalidate();\n }\n\n private setNotice(notice: string): void {\n this.notice = notice;\n this.invalidate();\n }\n\n private setQuery(query: string): void {\n if (query === this.query) return;\n this.query = query;\n // The previous index named a row this filter may no longer list.\n this.cursorIndex = 0;\n this.detailOffset = 0;\n this.invalidate();\n }\n\n private launchCursor(): void {\n const row = this.cursorRow();\n if (!row) {\n this.setNotice('launch unavailable · no workflow is selected');\n return;\n }\n const launchWorkflow = this.options.launchWorkflow;\n if (!launchWorkflow) {\n this.setNotice('launch unavailable · no workflow launcher is attached');\n return;\n }\n const detail = this.detailFor(row);\n if (detail.error) {\n this.setNotice(`launch unavailable · ${detail.error}`);\n return;\n }\n // Closing first is what makes the launcher's own prompts reachable: they\n // are host dialogs, and a fullscreen overlay sits over them.\n this.done(undefined);\n launchWorkflow(row);\n }\n\n private handleFilterInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.filtering = false;\n this.setQuery('');\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'enter')) {\n this.filtering = false;\n this.invalidate();\n return;\n }\n if (matchesKey(data, 'backspace')) {\n this.setQuery(this.query.slice(0, -1));\n return;\n }\n if (data === KEY_CLEAR) {\n this.setQuery('');\n return;\n }\n if (!isControlInput(data)) this.setQuery(this.query + data);\n }\n\n handleInput(data: string): void {\n if (this.filtering) {\n this.handleFilterInput(data);\n return;\n }\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, Key.shift('k'))) return this.scrollDetail(-1);\n if (matchesKey(data, Key.shift('j'))) return this.scrollDetail(1);\n if (matchesKey(data, 'up') || data === 'k') return this.moveCursor(-1);\n if (matchesKey(data, 'down') || data === 'j') return this.moveCursor(1);\n if (matchesKey(data, 'pageUp')) return this.moveCursor(-this.listPageSize);\n if (matchesKey(data, 'pageDown')) return this.moveCursor(this.listPageSize);\n if (matchesKey(data, 'tab')) return this.cycleTab();\n if (data === FILTER_KEY) {\n this.filtering = true;\n this.notice = undefined;\n this.invalidate();\n return;\n }\n if (data === LAUNCH_KEY) return this.launchCursor();\n const tab = TAB_KEYS[data];\n if (tab) this.switchTab(tab);\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: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: BREADCRUMB,\n headerRight: `${scope} workflows · ${this.tab}`,\n footer: this.filtering ? FILTER_FOOTER : FOOTER,\n footerHints: this.filtering ? FILTER_FOOTER_HINTS : FOOTER_HINTS,\n footerRight: matches.length\n ? `${Math.min(this.cursorIndex, matches.length - 1) + 1}/${matches.length}`\n : 'no match',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const filterRows = this.filtering || this.query ? 1 : 0;\n const transientRows = filterRows + (this.notice === undefined ? 0 : 1);\n const mainHeight = Math.max(0, height - transientRows);\n const layout = splitLayout(width);\n const left = this.renderList(layout.leftContentWidth, mainHeight);\n const right = this.renderDetail(layout.rightContentWidth, mainHeight);\n const gutter = ' '.repeat(layout.gutterWidth);\n const divider = this.theme.fg('borderMuted', DIVIDER);\n const lines: string[] = [];\n for (let index = 0; index < mainHeight; index++) {\n lines.push(\n `${fit(left[index] ?? '', layout.leftContentWidth)}${gutter}${divider}${gutter}${fit(\n right[index] ?? '',\n layout.rightContentWidth,\n )}`,\n );\n }\n if (filterRows) {\n const value = this.filtering ? `${this.query}${CURSOR_BLOCK}` : this.query;\n lines.push(` ${this.theme.bold(this.theme.fg('accent', FILTER_LABEL))} ${value}`);\n }\n if (this.notice !== undefined) lines.push(` ${this.theme.fg('muted', this.notice)}`);\n return lines.slice(0, height).map((line) => fit(line, width));\n }\n\n private renderList(width: number, height: number): string[] {\n const matches = this.matches();\n const lines = [this.theme.bold(this.theme.fg('accent', `WORKFLOWS ${matches.length}`))];\n if (matches.length === 0) {\n appendWrapped(\n lines,\n this.rows.length === 0 ? EMPTY_MESSAGE : 'No workflow matches this filter.',\n width,\n 'dim',\n this.theme,\n );\n this.listPageSize = DEFAULT_PAGE_SIZE;\n return lines.slice(0, height);\n }\n\n this.listPageSize = Math.max(DEFAULT_PAGE_SIZE, Math.floor((height - 1) / ROWS_PER_ENTRY));\n const active = Math.min(this.cursorIndex, matches.length - 1);\n const start = Math.max(\n 0,\n Math.min(active - this.listPageSize + 1, Math.max(0, matches.length - this.listPageSize)),\n );\n for (const [offset, row] of matches.slice(start, start + this.listPageSize).entries()) {\n const current = start + offset === active;\n const marker = current ? this.theme.fg('accent', SELECTION_MARKER) : ' ';\n const label = row.name || EMPTY_NAME;\n const name = current ? this.theme.bold(this.theme.fg('accent', label)) : this.theme.fg('text', label);\n const meta = this.theme.fg('dim', pathTail(row.relativePath, Math.max(0, width - META_INDENT.length)));\n lines.push(fit(`${marker} ${name}`, width));\n lines.push(fit(`${META_INDENT}${meta}`, width));\n }\n return lines.slice(0, height);\n }\n\n private renderDetail(width: number, height: number): string[] {\n const row = this.cursorRow();\n if (!row) {\n this.detailTotal = 0;\n this.detailPageSize = Math.max(DEFAULT_PAGE_SIZE, height);\n this.detailOffset = 0;\n const lines = [this.theme.bold(this.theme.fg('accent', 'WORKFLOW DETAIL')), ''];\n appendWrapped(\n lines,\n this.rows.length === 0 ? EMPTY_MESSAGE : 'No workflow matches this filter.',\n width,\n 'text',\n this.theme,\n );\n return lines.slice(0, height);\n }\n\n const header = [\n rightAligned(\n this.theme.bold(this.theme.fg('accent', `INSPECTING ${row.name || EMPTY_NAME}`)),\n this.theme.fg('dim', pathTail(row.relativePath, Math.max(0, Math.floor(width / 2)))),\n width,\n ),\n ];\n if (row.description) appendWrapped(header, row.description, width, 'text', this.theme);\n if (row.tags.length > 0) appendWrapped(header, row.tags.join(' · '), width, 'dim', this.theme);\n header.push(tabStrip(this.tab, width, this.theme), '');\n\n const body = workflowDetailLines(this.detailFor(row), this.tab, width, this.theme);\n this.detailPageSize = Math.max(0, height - header.length);\n this.detailTotal = body.length;\n this.detailOffset = Math.min(this.detailOffset, Math.max(0, body.length - this.detailPageSize));\n return [...header, ...body.slice(this.detailOffset, this.detailOffset + this.detailPageSize)].slice(0, height);\n }\n}\n\nexport async function openWorkflowCatalogOverlay(\n ctx: ExtensionContext,\n rows: readonly WorkflowCatalogRow[],\n options: WorkflowCatalogOptions,\n): Promise<void> {\n await ctx.ui.custom<undefined>(\n (tui, theme, _keybindings, done) => new WorkflowCatalogComponent(tui, theme, rows, done, options),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
@@ -1,3 +0,0 @@
1
- let e=require("@earendil-works/pi-tui");const t=`workflow-run-finished`,n=new Set([`stopped`,`paused`,`cancelled`]);function r(e,t){return e===`completed`?t.fg(`success`,`✔`):n.has(e)?t.fg(`warning`,`■`):t.fg(`error`,`✖`)}function i(e,t){let n=` ${t.fg(`dim`,`·`)} `,i=[e.runKey,e.workspace].filter(Boolean).join(`/`),a=[t.fg(`dim`,i),t.fg(`dim`,e.stage)];e.workflowId&&a.unshift(t.bold(e.workflowId));let o=`${r(e.stage,t)} ${a.join(n)}`;return e.failedJob&&(o+=`\n ${t.fg(`dim`,`⎿ failed job: ${e.failedJob}`)}`),e.error&&(o+=`\n ${t.fg(`dim`,`⎿ ${e.error}`)}`),o}function a(e,t){return e.map(e=>i(e,t)).join(`
2
- `)}function o(e){return!Array.isArray(e)||e.length===0?!1:e.every(e=>typeof e==`object`&&!!e&&typeof e.runKey==`string`&&typeof e.stage==`string`)}function s(){return[t,(t,n,r)=>{let i=typeof t.content==`string`?t.content:JSON.stringify(t.content),s=t.details?.runs;return o(s)?new e.Text(a(s,r),0,0):new e.Text(i,0,0)}]}exports.WORKFLOW_FINISHED_MESSAGE=t,exports.createWorkflowFinishedRenderer=s,exports.isWorkflowFinishedRuns=o,exports.renderWorkflowFinished=a;
3
- //# sourceMappingURL=workflowFinishedMessage.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"workflowFinishedMessage.cjs","names":["Text"],"sources":["../../../../src/tui/workflow/workflowFinishedMessage.ts"],"sourcesContent":["/**\n * Renders the `workflow-run-finished` custom message: one line per run that\n * reached a terminal state, rather than the host's default block of the\n * `[workflow-run-finished]` label above raw markdown.\n *\n * The same shape `@agimon-ai/doompi-team` gives a slash launch: a status glyph,\n * the run's identity, its outcome, and anything that went wrong indented under\n * it. A finished run is read at a glance in a transcript that is mostly about\n * something else, so it earns a line, not a card.\n *\n * DESIGN PATTERNS:\n * - A message with no `details` renders its content verbatim. That is a\n * transcript entry written before the field existed, and re-deriving it by\n * parsing the summary back out of markdown is exactly the liability\n * `completionNotice.ts` in the team package refused to port\n * - The full summary is still the message's `content`, which is what the model\n * reads and what `deliverAs: 'steer'` carries. This changes what a person\n * sees, never what the agent is told\n */\n\nimport type { ExtensionAPI, Theme } from '@earendil-works/pi-coding-agent';\nimport { type Component, Text } from '@earendil-works/pi-tui';\n\nexport const WORKFLOW_FINISHED_MESSAGE = 'workflow-run-finished';\n\n/** One finished run, as the extension records it on the message. */\nexport interface WorkflowFinishedRun {\n runKey: string;\n workspace: string;\n stage: string;\n workflowId?: string;\n failedJob?: string;\n error?: string;\n}\n\nexport interface WorkflowFinishedDetails {\n /** Kept for readers that already index runs by id; the lines come from `runs`. */\n runIds?: string[];\n runs?: WorkflowFinishedRun[];\n}\n\nconst COMPLETED_STAGE = 'completed';\nconst HELD_STAGES = new Set(['stopped', 'paused', 'cancelled']);\n\nfunction statusIcon(stage: string, theme: Theme): string {\n if (stage === COMPLETED_STAGE) return theme.fg('success', '✔');\n // Held is not failed: the run went nowhere wrong, it just stopped short.\n if (HELD_STAGES.has(stage)) return theme.fg('warning', '■');\n return theme.fg('error', '✖');\n}\n\nfunction runLine(run: WorkflowFinishedRun, theme: Theme): string {\n const separator = ` ${theme.fg('dim', '·')} `;\n const identity = [run.runKey, run.workspace].filter(Boolean).join('/');\n const parts = [theme.fg('dim', identity), theme.fg('dim', run.stage)];\n if (run.workflowId) parts.unshift(theme.bold(run.workflowId));\n let text = `${statusIcon(run.stage, theme)} ${parts.join(separator)}`;\n if (run.failedJob) text += `\\n ${theme.fg('dim', `⎿ failed job: ${run.failedJob}`)}`;\n if (run.error) text += `\\n ${theme.fg('dim', `⎿ ${run.error}`)}`;\n return text;\n}\n\n/**\n * Exported for tests: the rendered string before it is wrapped in a `Text`\n * component, so the formatting is assertable without a terminal.\n */\nexport function renderWorkflowFinished(runs: readonly WorkflowFinishedRun[], theme: Theme): string {\n return runs.map((run) => runLine(run, theme)).join('\\n');\n}\n\nexport function isWorkflowFinishedRuns(value: unknown): value is WorkflowFinishedRun[] {\n if (!Array.isArray(value) || value.length === 0) return false;\n return value.every(\n (entry) =>\n typeof entry === 'object' &&\n entry !== null &&\n typeof (entry as WorkflowFinishedRun).runKey === 'string' &&\n typeof (entry as WorkflowFinishedRun).stage === 'string',\n );\n}\n\nexport function createWorkflowFinishedRenderer(): readonly [...Parameters<ExtensionAPI['registerMessageRenderer']>] {\n return [\n WORKFLOW_FINISHED_MESSAGE,\n (message, _options, theme: Theme): Component => {\n const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);\n const runs = (message.details as WorkflowFinishedDetails | undefined)?.runs;\n if (!isWorkflowFinishedRuns(runs)) return new Text(content, 0, 0);\n return new Text(renderWorkflowFinished(runs, theme), 0, 0);\n },\n ];\n}\n"],"mappings":"wCAuBA,MAAa,EAA4B,wBAmBnC,EAAc,IAAI,IAAI,CAAC,UAAW,SAAU,WAAW,CAAC,EAE9D,SAAS,EAAW,EAAe,EAAsB,CAIvD,OAHI,IAAU,YAAwB,EAAM,GAAG,UAAW,GAAG,EAEzD,EAAY,IAAI,CAAK,EAAU,EAAM,GAAG,UAAW,GAAG,EACnD,EAAM,GAAG,QAAS,GAAG,CAC9B,CAEA,SAAS,EAAQ,EAA0B,EAAsB,CAC/D,IAAM,EAAY,IAAI,EAAM,GAAG,MAAO,GAAG,EAAE,GACrC,EAAW,CAAC,EAAI,OAAQ,EAAI,SAAS,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,EAC/D,EAAQ,CAAC,EAAM,GAAG,MAAO,CAAQ,EAAG,EAAM,GAAG,MAAO,EAAI,KAAK,CAAC,EAChE,EAAI,YAAY,EAAM,QAAQ,EAAM,KAAK,EAAI,UAAU,CAAC,EAC5D,IAAI,EAAO,GAAG,EAAW,EAAI,MAAO,CAAK,EAAE,GAAG,EAAM,KAAK,CAAS,IAGlE,OAFI,EAAI,YAAW,GAAQ,OAAO,EAAM,GAAG,MAAO,kBAAkB,EAAI,WAAW,KAC/E,EAAI,QAAO,GAAQ,OAAO,EAAM,GAAG,MAAO,MAAM,EAAI,OAAO,KACxD,CACT,CAMA,SAAgB,EAAuB,EAAsC,EAAsB,CACjG,OAAO,EAAK,IAAK,GAAQ,EAAQ,EAAK,CAAK,CAAC,CAAC,CAAC,KAAK;CAAI,CACzD,CAEA,SAAgB,EAAuB,EAAgD,CAErF,MADI,CAAC,MAAM,QAAQ,CAAK,GAAK,EAAM,SAAW,EAAU,GACjD,EAAM,MACV,GACC,OAAO,GAAU,YACjB,GACA,OAAQ,EAA8B,QAAW,UACjD,OAAQ,EAA8B,OAAU,QACpD,CACF,CAEA,SAAgB,GAAoG,CAClH,MAAO,CACL,GACC,EAAS,EAAU,IAA4B,CAC9C,IAAM,EAAU,OAAO,EAAQ,SAAY,SAAW,EAAQ,QAAU,KAAK,UAAU,EAAQ,OAAO,EAChG,EAAQ,EAAQ,SAAiD,KAEvE,OADK,EAAuB,CAAI,EACzB,IAAIA,EAAAA,KAAK,EAAuB,EAAM,CAAK,EAAG,EAAG,CAAC,EADf,IAAIA,EAAAA,KAAK,EAAS,EAAG,CAAC,CAElE,CACF,CACF"}
@@ -1,3 +0,0 @@
1
- import{Text as e}from"@earendil-works/pi-tui";const t=`workflow-run-finished`,n=new Set([`stopped`,`paused`,`cancelled`]);function r(e,t){return e===`completed`?t.fg(`success`,`✔`):n.has(e)?t.fg(`warning`,`■`):t.fg(`error`,`✖`)}function i(e,t){let n=` ${t.fg(`dim`,`·`)} `,i=[e.runKey,e.workspace].filter(Boolean).join(`/`),a=[t.fg(`dim`,i),t.fg(`dim`,e.stage)];e.workflowId&&a.unshift(t.bold(e.workflowId));let o=`${r(e.stage,t)} ${a.join(n)}`;return e.failedJob&&(o+=`\n ${t.fg(`dim`,`⎿ failed job: ${e.failedJob}`)}`),e.error&&(o+=`\n ${t.fg(`dim`,`⎿ ${e.error}`)}`),o}function a(e,t){return e.map(e=>i(e,t)).join(`
2
- `)}function o(e){return!Array.isArray(e)||e.length===0?!1:e.every(e=>typeof e==`object`&&!!e&&typeof e.runKey==`string`&&typeof e.stage==`string`)}function s(){return[t,(t,n,r)=>{let i=typeof t.content==`string`?t.content:JSON.stringify(t.content),s=t.details?.runs;return o(s)?new e(a(s,r),0,0):new e(i,0,0)}]}export{t as WORKFLOW_FINISHED_MESSAGE,s as createWorkflowFinishedRenderer,o as isWorkflowFinishedRuns,a as renderWorkflowFinished};
3
- //# sourceMappingURL=workflowFinishedMessage.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"workflowFinishedMessage.mjs","names":[],"sources":["../../../../src/tui/workflow/workflowFinishedMessage.ts"],"sourcesContent":["/**\n * Renders the `workflow-run-finished` custom message: one line per run that\n * reached a terminal state, rather than the host's default block of the\n * `[workflow-run-finished]` label above raw markdown.\n *\n * The same shape `@agimon-ai/doompi-team` gives a slash launch: a status glyph,\n * the run's identity, its outcome, and anything that went wrong indented under\n * it. A finished run is read at a glance in a transcript that is mostly about\n * something else, so it earns a line, not a card.\n *\n * DESIGN PATTERNS:\n * - A message with no `details` renders its content verbatim. That is a\n * transcript entry written before the field existed, and re-deriving it by\n * parsing the summary back out of markdown is exactly the liability\n * `completionNotice.ts` in the team package refused to port\n * - The full summary is still the message's `content`, which is what the model\n * reads and what `deliverAs: 'steer'` carries. This changes what a person\n * sees, never what the agent is told\n */\n\nimport type { ExtensionAPI, Theme } from '@earendil-works/pi-coding-agent';\nimport { type Component, Text } from '@earendil-works/pi-tui';\n\nexport const WORKFLOW_FINISHED_MESSAGE = 'workflow-run-finished';\n\n/** One finished run, as the extension records it on the message. */\nexport interface WorkflowFinishedRun {\n runKey: string;\n workspace: string;\n stage: string;\n workflowId?: string;\n failedJob?: string;\n error?: string;\n}\n\nexport interface WorkflowFinishedDetails {\n /** Kept for readers that already index runs by id; the lines come from `runs`. */\n runIds?: string[];\n runs?: WorkflowFinishedRun[];\n}\n\nconst COMPLETED_STAGE = 'completed';\nconst HELD_STAGES = new Set(['stopped', 'paused', 'cancelled']);\n\nfunction statusIcon(stage: string, theme: Theme): string {\n if (stage === COMPLETED_STAGE) return theme.fg('success', '✔');\n // Held is not failed: the run went nowhere wrong, it just stopped short.\n if (HELD_STAGES.has(stage)) return theme.fg('warning', '■');\n return theme.fg('error', '✖');\n}\n\nfunction runLine(run: WorkflowFinishedRun, theme: Theme): string {\n const separator = ` ${theme.fg('dim', '·')} `;\n const identity = [run.runKey, run.workspace].filter(Boolean).join('/');\n const parts = [theme.fg('dim', identity), theme.fg('dim', run.stage)];\n if (run.workflowId) parts.unshift(theme.bold(run.workflowId));\n let text = `${statusIcon(run.stage, theme)} ${parts.join(separator)}`;\n if (run.failedJob) text += `\\n ${theme.fg('dim', `⎿ failed job: ${run.failedJob}`)}`;\n if (run.error) text += `\\n ${theme.fg('dim', `⎿ ${run.error}`)}`;\n return text;\n}\n\n/**\n * Exported for tests: the rendered string before it is wrapped in a `Text`\n * component, so the formatting is assertable without a terminal.\n */\nexport function renderWorkflowFinished(runs: readonly WorkflowFinishedRun[], theme: Theme): string {\n return runs.map((run) => runLine(run, theme)).join('\\n');\n}\n\nexport function isWorkflowFinishedRuns(value: unknown): value is WorkflowFinishedRun[] {\n if (!Array.isArray(value) || value.length === 0) return false;\n return value.every(\n (entry) =>\n typeof entry === 'object' &&\n entry !== null &&\n typeof (entry as WorkflowFinishedRun).runKey === 'string' &&\n typeof (entry as WorkflowFinishedRun).stage === 'string',\n );\n}\n\nexport function createWorkflowFinishedRenderer(): readonly [...Parameters<ExtensionAPI['registerMessageRenderer']>] {\n return [\n WORKFLOW_FINISHED_MESSAGE,\n (message, _options, theme: Theme): Component => {\n const content = typeof message.content === 'string' ? message.content : JSON.stringify(message.content);\n const runs = (message.details as WorkflowFinishedDetails | undefined)?.runs;\n if (!isWorkflowFinishedRuns(runs)) return new Text(content, 0, 0);\n return new Text(renderWorkflowFinished(runs, theme), 0, 0);\n },\n ];\n}\n"],"mappings":"8CAuBA,MAAa,EAA4B,wBAmBnC,EAAc,IAAI,IAAI,CAAC,UAAW,SAAU,WAAW,CAAC,EAE9D,SAAS,EAAW,EAAe,EAAsB,CAIvD,OAHI,IAAU,YAAwB,EAAM,GAAG,UAAW,GAAG,EAEzD,EAAY,IAAI,CAAK,EAAU,EAAM,GAAG,UAAW,GAAG,EACnD,EAAM,GAAG,QAAS,GAAG,CAC9B,CAEA,SAAS,EAAQ,EAA0B,EAAsB,CAC/D,IAAM,EAAY,IAAI,EAAM,GAAG,MAAO,GAAG,EAAE,GACrC,EAAW,CAAC,EAAI,OAAQ,EAAI,SAAS,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,EAC/D,EAAQ,CAAC,EAAM,GAAG,MAAO,CAAQ,EAAG,EAAM,GAAG,MAAO,EAAI,KAAK,CAAC,EAChE,EAAI,YAAY,EAAM,QAAQ,EAAM,KAAK,EAAI,UAAU,CAAC,EAC5D,IAAI,EAAO,GAAG,EAAW,EAAI,MAAO,CAAK,EAAE,GAAG,EAAM,KAAK,CAAS,IAGlE,OAFI,EAAI,YAAW,GAAQ,OAAO,EAAM,GAAG,MAAO,kBAAkB,EAAI,WAAW,KAC/E,EAAI,QAAO,GAAQ,OAAO,EAAM,GAAG,MAAO,MAAM,EAAI,OAAO,KACxD,CACT,CAMA,SAAgB,EAAuB,EAAsC,EAAsB,CACjG,OAAO,EAAK,IAAK,GAAQ,EAAQ,EAAK,CAAK,CAAC,CAAC,CAAC,KAAK;CAAI,CACzD,CAEA,SAAgB,EAAuB,EAAgD,CAErF,MADI,CAAC,MAAM,QAAQ,CAAK,GAAK,EAAM,SAAW,EAAU,GACjD,EAAM,MACV,GACC,OAAO,GAAU,YACjB,GACA,OAAQ,EAA8B,QAAW,UACjD,OAAQ,EAA8B,OAAU,QACpD,CACF,CAEA,SAAgB,GAAoG,CAClH,MAAO,CACL,GACC,EAAS,EAAU,IAA4B,CAC9C,IAAM,EAAU,OAAO,EAAQ,SAAY,SAAW,EAAQ,QAAU,KAAK,UAAU,EAAQ,OAAO,EAChG,EAAQ,EAAQ,SAAiD,KAEvE,OADK,EAAuB,CAAI,EACzB,IAAI,EAAK,EAAuB,EAAM,CAAK,EAAG,EAAG,CAAC,EADf,IAAI,EAAK,EAAS,EAAG,CAAC,CAElE,CACF,CACF"}
@@ -1,2 +0,0 @@
1
- const e=require("./doomOverlay.cjs"),t=require("./overlayText.cjs"),n=require("./workflowStatusRow.cjs");let r=require("@agimon-ai/workflow-mcp"),i=require("@earendil-works/pi-tui");e.DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;const a=`running`,o=`paused`,s=`↑↓`,c=`${s} select · PgUp/PgDn page · enter open · esc close`,l=[[s,`select`],[`PgUp/PgDn`,`page`],[`enter`,`open`],[`esc`,`close`]];function u(e,t){let n=(0,i.truncateToWidth)(e,Math.max(0,t),`…`).replaceAll(`\x1B[0m`,``);return n+` `.repeat(Math.max(0,t-(0,i.visibleWidth)(n)))}function d(e,t,n){let r=(0,i.visibleWidth)(t),a=Math.max(0,n-r-1);return u(e,a)+` `.repeat(Math.max(1,n-a-r))+u(t,r)}function f(e){return e===`completed`?`✓`:e===`failed`?`✗`:`●`}function p(e){return e===`completed`?`success`:e===`failed`?`error`:`accent`}function m(e,t,n){let r=[];for(let o of e.jobs){let e=o.total?` · ${(o.index??0)+1}/${o.total}`:``;if(r.push((0,i.truncateToWidth)(n.fg(p(o.status),`${f(o.status)} ${o.name}`)+n.fg(`dim`,e),t,`…`)),o.status===a)for(let e of o.steps){let o=e.status===`failed`?`error`:e.status===a?`muted`:`dim`;r.push((0,i.truncateToWidth)(n.fg(o,` ${f(e.status)} ${e.name}`),t,`…`))}}return r}function h(e,s,c,l){if(!e)return[l.fg(`dim`,`Select a workflow to inspect its jobs and output.`)];let u=(0,r.currentWorkflowPosition)(e.jobs),f=n.humanizeDuration(Math.max(0,Date.now()-Date.parse(e.startedAt))),p=e.executionState??a,h=p===o?`warning`:`dim`,g=[d(`${l.fg(`accent`,`●`)} ${l.bold(e.displayName)}`,l.fg(h,p),s),(0,i.truncateToWidth)(l.fg(`dim`,`${e.runKey} · ${e.workspace} · ${f}`),s,`…`),u?(0,i.truncateToWidth)(`${l.fg(`accent`,u.job)}${u.step?l.fg(`muted`,` · ${u.step}`):``}`,s,`…`):l.fg(`dim`,`starting`),``,l.fg(`dim`,`PROGRESS`)],_=e.output.length>0?3:0,v=Math.max(0,c-g.length-_);if(g.push(...m(e,s,l).slice(0,v)),e.output.length>0&&c-g.length>=2){g.push(``,l.fg(`dim`,`OUTPUT · newest last`));let n=Math.max(0,c-g.length);g.push(...e.output.slice(-n).map(e=>t.fitTerminalLine(e,s)))}return g.slice(0,c)}var g=class extends e.DoomOverlay{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((0,i.matchesKey)(e,`escape`)||(0,i.matchesKey)(e,`ctrl+c`)){this.finish(void 0);return}if((0,i.matchesKey)(e,`up`)||e===`k`){this.move(-1);return}if((0,i.matchesKey)(e,`down`)||e===`j`){this.move(1);return}if((0,i.matchesKey)(e,`pageUp`)){this.move(-this.rosterCapacity());return}if((0,i.matchesKey)(e,`pageDown`)){this.move(this.rosterCapacity());return}if((0,i.matchesKey)(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 t=this.current(),n=t?`${t.displayName} · ${t.executionState??a}`:`no active workflows`;return{title:`WORKFLOW SPACE`,accent:e.DOOM_OVERLAY_ACCENT,breadcrumb:`SPC › w / workflows › l / manage`,headerRight:`${this.items.length} running · ${n}`,footer:c,footerHints:l,footerRight:this.items.length>0?`${Math.min(this.selected,this.items.length-1)+1}/${this.items.length}`:`empty`}}renderBody(e,n){this.bodyHeight=n;let r=e=>this.error?[this.theme.fg(`warning`,`Unable to refresh workflows: ${this.error}`)]:h(this.current(),e,n,this.theme);if(e<48)return r(e).slice(0,n);let a=Math.max(18,Math.floor((e-1)*.3333333333333333)),o=Math.max(1,e-a-1),s=Math.max(1,a-1),c=Math.max(1,o-1),l=this.rosterLines(s),d=r(c),f=this.theme.fg(`borderMuted`,`│`);return Array.from({length:n},(n,r)=>{let a=`${u(l[r]??``,s)} ${f} ${t.fitTerminalLine(d[r]??``,c)}`;return(0,i.truncateToWidth)(a,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(),i=Math.min(this.selected,this.items.length-1),a=Math.max(0,Math.min(i-n+1,Math.max(0,this.items.length-n)));for(let[s,c]of this.items.slice(a,a+n).entries()){let n=a+s===i,l=c.executionState===o,d=n?this.theme.fg(`accent`,`›`):` `,f=this.theme.fg(l?`warning`:`accent`,`●`),p=n?this.theme.bold(c.displayName):c.displayName,m=l?o:(0,r.currentWorkflowPosition)(c.jobs)?.job??`starting`,h=`${d} ${f} ${p}`,g=`${` `.repeat(4)}${this.theme.fg(l?`warning`:`dim`,m)}`,_=n?`selectedBg`:`userMessageBg`;for(let n of[h,g])t.push(this.theme.bg(_,u(n,e)))}return t}invalidate(){this.refresh()}dispose(){this.completed||(this.completed=!0,clearInterval(this.timer))}};exports.WorkflowInspectorComponent=g;
2
- //# sourceMappingURL=workflowInspector.cjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"workflowInspector.cjs","names":["DOOM_FULLSCREEN_UI_OPTIONS","truncateToWidth","visibleWidth","currentWorkflowPosition","humanizeDuration","fitTerminalLine","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';\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay';\nimport { fitTerminalLine } from './overlayText';\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 // Captured output carries the run's own colour, so it is fitted by the\n // escape-preserving path: `fit` below drops hard resets, and a capture that\n // lost its reset bleeds its colour through the divider and down the pane.\n lines.push(...item.output.slice(-outputRoom).map((line) => fitTerminalLine(line, width)));\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 // The roster keeps `fit`, whose reset stripping protects its row\n // backgrounds; the detail pane has none and may carry captured colour.\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fitTerminalLine(\n right[index] ?? '',\n rightContent,\n )}`;\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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"}
@@ -1,2 +0,0 @@
1
- import{DOOM_FULLSCREEN_UI_OPTIONS as e,DOOM_OVERLAY_ACCENT as t,DoomOverlay as n}from"./doomOverlay.mjs";import{fitTerminalLine as r}from"./overlayText.mjs";import{humanizeDuration as i}from"./workflowStatusRow.mjs";import{currentWorkflowPosition as a}from"@agimon-ai/workflow-mcp";import{matchesKey as o,truncateToWidth as s,visibleWidth as c}from"@earendil-works/pi-tui";e.overlayOptions;const l=`running`,u=`paused`,d=`↑↓`,f=`${d} select · PgUp/PgDn page · enter open · esc close`,p=[[d,`select`],[`PgUp/PgDn`,`page`],[`enter`,`open`],[`esc`,`close`]];function m(e,t){let n=s(e,Math.max(0,t),`…`).replaceAll(`\x1B[0m`,``);return n+` `.repeat(Math.max(0,t-c(n)))}function h(e,t,n){let r=c(t),i=Math.max(0,n-r-1);return m(e,i)+` `.repeat(Math.max(1,n-i-r))+m(t,r)}function g(e){return e===`completed`?`✓`:e===`failed`?`✗`:`●`}function _(e){return e===`completed`?`success`:e===`failed`?`error`:`accent`}function v(e,t,n){let r=[];for(let i of e.jobs){let e=i.total?` · ${(i.index??0)+1}/${i.total}`:``;if(r.push(s(n.fg(_(i.status),`${g(i.status)} ${i.name}`)+n.fg(`dim`,e),t,`…`)),i.status===l)for(let e of i.steps){let i=e.status===`failed`?`error`:e.status===l?`muted`:`dim`;r.push(s(n.fg(i,` ${g(e.status)} ${e.name}`),t,`…`))}}return r}function y(e,t,n,o){if(!e)return[o.fg(`dim`,`Select a workflow to inspect its jobs and output.`)];let c=a(e.jobs),d=i(Math.max(0,Date.now()-Date.parse(e.startedAt))),f=e.executionState??l,p=f===u?`warning`:`dim`,m=[h(`${o.fg(`accent`,`●`)} ${o.bold(e.displayName)}`,o.fg(p,f),t),s(o.fg(`dim`,`${e.runKey} · ${e.workspace} · ${d}`),t,`…`),c?s(`${o.fg(`accent`,c.job)}${c.step?o.fg(`muted`,` · ${c.step}`):``}`,t,`…`):o.fg(`dim`,`starting`),``,o.fg(`dim`,`PROGRESS`)],g=e.output.length>0?3:0,_=Math.max(0,n-m.length-g);if(m.push(...v(e,t,o).slice(0,_)),e.output.length>0&&n-m.length>=2){m.push(``,o.fg(`dim`,`OUTPUT · newest last`));let i=Math.max(0,n-m.length);m.push(...e.output.slice(-i).map(e=>r(e,t)))}return m.slice(0,n)}var b=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(o(e,`escape`)||o(e,`ctrl+c`)){this.finish(void 0);return}if(o(e,`up`)||e===`k`){this.move(-1);return}if(o(e,`down`)||e===`j`){this.move(1);return}if(o(e,`pageUp`)){this.move(-this.rosterCapacity());return}if(o(e,`pageDown`)){this.move(this.rosterCapacity());return}if(o(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??l}`:`no active workflows`;return{title:`WORKFLOW SPACE`,accent:t,breadcrumb:`SPC › w / workflows › l / manage`,headerRight:`${this.items.length} running · ${n}`,footer:f,footerHints:p,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}`)]:y(this.current(),e,t,this.theme);if(e<48)return n(e).slice(0,t);let i=Math.max(18,Math.floor((e-1)*.3333333333333333)),a=Math.max(1,e-i-1),o=Math.max(1,i-1),c=Math.max(1,a-1),l=this.rosterLines(o),u=n(c),d=this.theme.fg(`borderMuted`,`│`);return Array.from({length:t},(t,n)=>{let i=`${m(l[n]??``,o)} ${d} ${r(u[n]??``,c)}`;return s(i,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),i=Math.max(0,Math.min(r-n+1,Math.max(0,this.items.length-n)));for(let[o,s]of this.items.slice(i,i+n).entries()){let n=i+o===r,c=s.executionState===u,l=n?this.theme.fg(`accent`,`›`):` `,d=this.theme.fg(c?`warning`:`accent`,`●`),f=n?this.theme.bold(s.displayName):s.displayName,p=c?u:a(s.jobs)?.job??`starting`,h=`${l} ${d} ${f}`,g=`${` `.repeat(4)}${this.theme.fg(c?`warning`:`dim`,p)}`,_=n?`selectedBg`:`userMessageBg`;for(let n of[h,g])t.push(this.theme.bg(_,m(n,e)))}return t}invalidate(){this.refresh()}dispose(){this.completed||(this.completed=!0,clearInterval(this.timer))}};export{b as WorkflowInspectorComponent};
2
- //# sourceMappingURL=workflowInspector.mjs.map
@@ -1 +0,0 @@
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';\n\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay';\nimport { fitTerminalLine } from './overlayText';\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 // Captured output carries the run's own colour, so it is fitted by the\n // escape-preserving path: `fit` below drops hard resets, and a capture that\n // lost its reset bleeds its colour through the divider and down the pane.\n lines.push(...item.output.slice(-outputRoom).map((line) => fitTerminalLine(line, width)));\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 // The roster keeps `fit`, whose reset stripping protects its row\n // backgrounds; the detail pane has none and may carry captured colour.\n const row = `${fit(left[index] ?? '', leftContent)} ${divider} ${fitTerminalLine(\n right[index] ?? '',\n rightContent,\n )}`;\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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"}
@@ -1,2 +0,0 @@
1
- let e=require("@earendil-works/pi-tui");var t=class{windowMs;lastEscapeAt;constructor(e=350){this.windowMs=e}observe(t,n=Date.now()){return t===`\x1B\x1B`?(this.lastEscapeAt=void 0,`close`):(0,e.isKeyRelease)(t)?`forward`:(0,e.matchesKey)(t,`escape`)?this.lastEscapeAt!==void 0&&n-this.lastEscapeAt<=this.windowMs?(this.lastEscapeAt=void 0,`close`):(this.lastEscapeAt=n,`forward`):(this.lastEscapeAt=void 0,`forward`)}},n=class{send;flushIntervalMs;pending=``;timer;disposed=!1;constructor(e,t=16){this.send=e,this.flushIntervalMs=t}write(e){this.disposed||e.length===0||(this.pending+=e,!this.timer&&(this.timer=setTimeout(()=>void this.flush(),this.flushIntervalMs),this.timer.unref?.()))}async flush(){this.timer&&clearTimeout(this.timer),this.timer=void 0;let e=this.pending;if(this.pending=``,e&&!this.disposed)try{await this.send(e)}catch{}}dispose(){this.disposed=!0,this.timer&&clearTimeout(this.timer),this.timer=void 0,this.pending=``}hasPending(){return this.pending.length>0}};exports.DoubleEscapeDetector=t,exports.TerminalInputBatcher=n;
2
- //# sourceMappingURL=workflowOverlay.cjs.map
@@ -1 +0,0 @@
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,GACV,GAAQ,MAAK,SAClB,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"}
@@ -1,2 +0,0 @@
1
- import{isKeyRelease as e,matchesKey as t}from"@earendil-works/pi-tui";var n=class{windowMs;lastEscapeAt;constructor(e=350){this.windowMs=e}observe(n,r=Date.now()){return n===`\x1B\x1B`?(this.lastEscapeAt=void 0,`close`):e(n)?`forward`:t(n,`escape`)?this.lastEscapeAt!==void 0&&r-this.lastEscapeAt<=this.windowMs?(this.lastEscapeAt=void 0,`close`):(this.lastEscapeAt=r,`forward`):(this.lastEscapeAt=void 0,`forward`)}},r=class{send;flushIntervalMs;pending=``;timer;disposed=!1;constructor(e,t=16){this.send=e,this.flushIntervalMs=t}write(e){this.disposed||e.length===0||(this.pending+=e,!this.timer&&(this.timer=setTimeout(()=>void this.flush(),this.flushIntervalMs),this.timer.unref?.()))}async flush(){this.timer&&clearTimeout(this.timer),this.timer=void 0;let e=this.pending;if(this.pending=``,e&&!this.disposed)try{await this.send(e)}catch{}}dispose(){this.disposed=!0,this.timer&&clearTimeout(this.timer),this.timer=void 0,this.pending=``}hasPending(){return this.pending.length>0}};export{n as DoubleEscapeDetector,r as TerminalInputBatcher};
2
- //# sourceMappingURL=workflowOverlay.mjs.map
@@ -1 +0,0 @@
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,GACV,GAAQ,MAAK,SAClB,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"}
@@ -1,2 +0,0 @@
1
- const e=require("./doomOverlay.cjs"),t=require("./overlayText.cjs");let n=require("@earendil-works/pi-tui");e.DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;const r=`selectedBg`;function i(e,n){return n.trim()?e.filter(e=>t.matchesQuery([e.name,e.detail,e.search??``].join(` `),n)):[...e]}var a=class extends e.DoomOverlay{config;rows;done;query=``;selected=0;bodyHeight=1;constructor(e,t,n,r,i){super(e,t),this.config=n,this.rows=r,this.done=i}matches(){return i(this.rows,this.query)}capacity(){return Math.max(1,this.bodyHeight-3)}handleInput(e){if((0,n.matchesKey)(e,`escape`)||(0,n.matchesKey)(e,`ctrl+c`)){this.done(void 0);return}let r=this.matches();if((0,n.matchesKey)(e,`enter`)){let e=r[Math.min(this.selected,r.length-1)];e&&this.done(e.value);return}if((0,n.matchesKey)(e,`up`)||(0,n.matchesKey)(e,`ctrl+p`))this.move(-1,r.length);else if((0,n.matchesKey)(e,`down`)||(0,n.matchesKey)(e,`ctrl+n`))this.move(1,r.length);else if((0,n.matchesKey)(e,`pageUp`))this.move(-this.capacity(),r.length);else if((0,n.matchesKey)(e,`pageDown`))this.move(this.capacity(),r.length);else if((0,n.matchesKey)(e,`backspace`))this.setQuery(this.query.slice(0,-1));else if(e===``)this.setQuery(``);else if(!t.isControlInput(e))this.setQuery(this.query+e);else return;this.tui.requestRender()}setQuery(e){e!==this.query&&(this.query=e,this.selected=0)}move(e,t){t!==0&&(this.selected=Math.max(0,Math.min(t-1,this.selected+e)))}getChrome(){let t=this.matches(),n=this.query?`${t.length} of ${this.rows.length}`:`${this.rows.length}`;return{title:this.config.title,accent:e.DOOM_OVERLAY_ACCENT,breadcrumb:this.config.breadcrumb,headerRight:`${n} ${this.config.unit}`,footer:`↑↓ select · enter ${this.config.action} · type to filter · ctrl+u clear · esc cancel`,footerHints:[[`↑↓`,`select`],[`enter`,this.config.action],[`type`,`filter`],[`ctrl+u`,`clear`],[`esc`,`cancel`]],footerRight:t.length>0?`${Math.min(this.selected,t.length-1)+1}/${t.length}`:`no match`}}renderBody(e,t){return this.bodyHeight=t,[...this.filterField(e),...this.listLines(e)].slice(0,t)}filterField(e){let n=this.query?`${this.query}█`:`█${this.theme.fg(`dim`,this.config.filterPlaceholder)}`;return[this.theme.fg(`dim`,`FILTER`),this.theme.bg(r,t.fit(` ${n}`,e)),``]}listLines(e){if(this.rows.length===0)return[this.theme.fg(`dim`,this.config.emptyMessage)];let n=this.matches();if(n.length===0)return[this.theme.fg(`dim`,`No ${this.config.unit} match "${this.query}".`)];let i=this.capacity(),a=Math.min(this.selected,n.length-1),o=Math.max(0,Math.min(a-i+1,Math.max(0,n.length-i))),s=Math.max(16,Math.floor((e-2-2)*.45)),c=Math.max(0,e-2-2-s);return n.slice(o,o+i).map((n,i)=>{let l=o+i===a,u=l?this.theme.fg(`accent`,`›`):` `,d=n.name||`(unnamed)`,f=l?this.theme.bold(t.fit(d,s)):t.fit(d,s),p=this.theme.fg(`dim`,t.fit(t.pathTail(n.detail,c),c)),m=l?r:`userMessageBg`;return this.theme.bg(m,t.fit(`${u} ${f}${` `.repeat(2)}${p}`,e))})}};async function o(t,n,r){return t.ui.custom((e,t,i,o)=>new a(e,t,n,r,o),e.DOOM_FULLSCREEN_UI_OPTIONS)}exports.WorkflowPickerComponent=a,exports.filterPickerRows=i,exports.openWorkflowPickerOverlay=o;
2
- //# sourceMappingURL=workflowPicker.cjs.map