@agimon-ai/doompi-workflow 0.0.1-alpha.0

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 (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/dispatcher.cjs +2 -0
  4. package/dist/dispatcher.cjs.map +1 -0
  5. package/dist/dispatcher.d.cts +6 -0
  6. package/dist/dispatcher.d.cts.map +1 -0
  7. package/dist/dispatcher.d.mts +6 -0
  8. package/dist/dispatcher.d.mts.map +1 -0
  9. package/dist/dispatcher.mjs +2 -0
  10. package/dist/dispatcher.mjs.map +1 -0
  11. package/dist/extension.cjs +2 -0
  12. package/dist/extension.cjs.map +1 -0
  13. package/dist/extension.d.cts +6 -0
  14. package/dist/extension.d.cts.map +1 -0
  15. package/dist/extension.d.mts +6 -0
  16. package/dist/extension.d.mts.map +1 -0
  17. package/dist/extension.mjs +2 -0
  18. package/dist/extension.mjs.map +1 -0
  19. package/dist/extensions/dispatcher.cjs +2 -0
  20. package/dist/extensions/dispatcher.cjs.map +1 -0
  21. package/dist/extensions/dispatcher.d.cts +2 -0
  22. package/dist/extensions/dispatcher.d.mts +2 -0
  23. package/dist/extensions/dispatcher.mjs +2 -0
  24. package/dist/extensions/dispatcher.mjs.map +1 -0
  25. package/dist/extensions/pi.cjs +2 -0
  26. package/dist/extensions/pi.cjs.map +1 -0
  27. package/dist/extensions/pi.d.cts +2 -0
  28. package/dist/extensions/pi.d.mts +2 -0
  29. package/dist/extensions/pi.mjs +2 -0
  30. package/dist/extensions/pi.mjs.map +1 -0
  31. package/dist/index.cjs +1 -0
  32. package/dist/index.d.cts +3 -0
  33. package/dist/index.d.mts +3 -0
  34. package/dist/index.mjs +1 -0
  35. package/dist/leader.cjs +2 -0
  36. package/dist/leader.cjs.map +1 -0
  37. package/dist/leader.mjs +2 -0
  38. package/dist/leader.mjs.map +1 -0
  39. package/dist/types.cjs +2 -0
  40. package/dist/types.cjs.map +1 -0
  41. package/dist/types.mjs +2 -0
  42. package/dist/types.mjs.map +1 -0
  43. package/dist/workflow/backgroundWork.cjs +2 -0
  44. package/dist/workflow/backgroundWork.cjs.map +1 -0
  45. package/dist/workflow/backgroundWork.mjs +2 -0
  46. package/dist/workflow/backgroundWork.mjs.map +1 -0
  47. package/dist/workflow/doomOverlay.cjs +2 -0
  48. package/dist/workflow/doomOverlay.cjs.map +1 -0
  49. package/dist/workflow/doomOverlay.mjs +2 -0
  50. package/dist/workflow/doomOverlay.mjs.map +1 -0
  51. package/dist/workflow/overlayText.cjs +2 -0
  52. package/dist/workflow/overlayText.cjs.map +1 -0
  53. package/dist/workflow/overlayText.mjs +2 -0
  54. package/dist/workflow/overlayText.mjs.map +1 -0
  55. package/dist/workflow/piExtension.cjs +10 -0
  56. package/dist/workflow/piExtension.cjs.map +1 -0
  57. package/dist/workflow/piExtension.mjs +10 -0
  58. package/dist/workflow/piExtension.mjs.map +1 -0
  59. package/dist/workflow/piToolBridge.cjs +11 -0
  60. package/dist/workflow/piToolBridge.cjs.map +1 -0
  61. package/dist/workflow/piToolBridge.mjs +11 -0
  62. package/dist/workflow/piToolBridge.mjs.map +1 -0
  63. package/dist/workflow/piTools.cjs +4 -0
  64. package/dist/workflow/piTools.cjs.map +1 -0
  65. package/dist/workflow/piTools.mjs +4 -0
  66. package/dist/workflow/piTools.mjs.map +1 -0
  67. package/dist/workflow/rendering.cjs +2 -0
  68. package/dist/workflow/rendering.cjs.map +1 -0
  69. package/dist/workflow/rendering.mjs +2 -0
  70. package/dist/workflow/rendering.mjs.map +1 -0
  71. package/dist/workflow/workflowFooterStatus.cjs +2 -0
  72. package/dist/workflow/workflowFooterStatus.cjs.map +1 -0
  73. package/dist/workflow/workflowFooterStatus.mjs +2 -0
  74. package/dist/workflow/workflowFooterStatus.mjs.map +1 -0
  75. package/dist/workflow/workflowInspector.cjs +2 -0
  76. package/dist/workflow/workflowInspector.cjs.map +1 -0
  77. package/dist/workflow/workflowInspector.mjs +2 -0
  78. package/dist/workflow/workflowInspector.mjs.map +1 -0
  79. package/dist/workflow/workflowLauncher.cjs +2 -0
  80. package/dist/workflow/workflowLauncher.cjs.map +1 -0
  81. package/dist/workflow/workflowLauncher.mjs +2 -0
  82. package/dist/workflow/workflowLauncher.mjs.map +1 -0
  83. package/dist/workflow/workflowOverlay.cjs +2 -0
  84. package/dist/workflow/workflowOverlay.cjs.map +1 -0
  85. package/dist/workflow/workflowOverlay.mjs +2 -0
  86. package/dist/workflow/workflowOverlay.mjs.map +1 -0
  87. package/dist/workflow/workflowPicker.cjs +2 -0
  88. package/dist/workflow/workflowPicker.cjs.map +1 -0
  89. package/dist/workflow/workflowPicker.mjs +2 -0
  90. package/dist/workflow/workflowPicker.mjs.map +1 -0
  91. package/dist/workflow/workflowPrompt.cjs +2 -0
  92. package/dist/workflow/workflowPrompt.cjs.map +1 -0
  93. package/dist/workflow/workflowPrompt.mjs +2 -0
  94. package/dist/workflow/workflowPrompt.mjs.map +1 -0
  95. package/dist/workflow/workflowRunPanel.cjs +2 -0
  96. package/dist/workflow/workflowRunPanel.cjs.map +1 -0
  97. package/dist/workflow/workflowRunPanel.mjs +2 -0
  98. package/dist/workflow/workflowRunPanel.mjs.map +1 -0
  99. package/dist/workflow/workflowStatusRow.cjs +2 -0
  100. package/dist/workflow/workflowStatusRow.cjs.map +1 -0
  101. package/dist/workflow/workflowStatusRow.mjs +2 -0
  102. package/dist/workflow/workflowStatusRow.mjs.map +1 -0
  103. package/dist/workflow/workflowStepMessage.cjs +2 -0
  104. package/dist/workflow/workflowStepMessage.cjs.map +1 -0
  105. package/dist/workflow/workflowStepMessage.mjs +2 -0
  106. package/dist/workflow/workflowStepMessage.mjs.map +1 -0
  107. package/package.json +82 -0
  108. package/skills/workflow-recovery/SKILL.md +73 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowPrompt.cjs","names":["DOOM_FULLSCREEN_UI_OPTIONS","DoomOverlay","matchesKey","DOOM_OVERLAY_ACCENT","fit","rightAligned","isControlInput"],"sources":["../../src/workflow/workflowPrompt.ts"],"sourcesContent":["/**\n * The short prompts: pick one of a few options, or type one value.\n *\n * These were `ui.select` and `ui.input`, which meant the run manager, the runner\n * picker and every `workflow_dispatch` input rendered as native Pi widgets in the\n * middle of a flow whose other steps are framed doom overlays. Same surface for\n * all of them now, so a launch reads as one sequence rather than three styles.\n *\n * Rendering is pure, so both modes can be asserted as text without a terminal.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport {\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n DOOM_FULLSCREEN_UI_OPTIONS,\n} from './doomOverlay.ts';\nimport { matchesKey } from '@earendil-works/pi-tui';\nimport { CURSOR_BLOCK, fit, isControlInput, rightAligned, SELECTION_MARKER } from './overlayText';\n\nexport const WORKFLOW_PROMPT_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst CHOICE_TITLE = 'WORKFLOW PROMPT';\nconst CHOOSE_LABEL = 'CHOOSE';\nconst VALUE_LABEL = 'VALUE';\nconst DEFAULT_MARKER = 'default';\nconst EMPTY_CHOICES = 'No options are available.';\nconst CHOICE_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'select'],\n ['enter', 'confirm'],\n ['esc', 'cancel'],\n];\nconst INPUT_HINTS: readonly (readonly [string, string])[] = [\n ['type', 'edit'],\n ['enter', 'confirm'],\n ['ctrl+u', 'clear'],\n ['esc', 'cancel'],\n];\nconst KEY_CLEAR = '\\x15';\n/** Rows the heading and its spacer take before the first option. */\nconst HEADING_ROWS = 2;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\nconst DIM: ThemeColor = 'dim';\nconst ACCENT: ThemeColor = 'accent';\nconst SELECTED_BG: ThemeBg = 'selectedBg';\nconst ROW_BG: ThemeBg = 'userMessageBg';\n\nexport interface WorkflowChoiceOptions {\n title: string;\n breadcrumb: string;\n choices: readonly string[];\n /** Marked `default` in the list and pre-selected. */\n preselect?: string;\n}\n\nexport interface WorkflowInputOptions {\n title: string;\n breadcrumb: string;\n /** Committed when the field is left empty, and shown beside the label. */\n fallback?: string;\n}\n\nexport class WorkflowChoiceComponent extends DoomOverlay {\n private selected: number;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: WorkflowChoiceOptions,\n private readonly done: (choice: string | undefined) => void,\n ) {\n super(tui, theme);\n this.selected = Math.max(0, options.preselect ? options.choices.indexOf(options.preselect) : 0);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, 'enter')) {\n const choice = this.options.choices[this.selected];\n if (choice !== undefined) this.done(choice);\n return;\n }\n const count = this.options.choices.length;\n if (count === 0) return;\n if (matchesKey(data, 'up') || matchesKey(data, 'ctrl+p')) this.selected = Math.max(0, this.selected - 1);\n else if (matchesKey(data, 'down') || matchesKey(data, 'ctrl+n'))\n this.selected = Math.min(count - 1, this.selected + 1);\n else return;\n this.tui.requestRender();\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: CHOICE_TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.options.breadcrumb,\n headerRight: `${this.options.choices.length} options`,\n footer: '↑↓ select · enter confirm · esc cancel',\n footerHints: CHOICE_HINTS,\n footerRight:\n this.options.choices.length > 0 ? `${this.selected + 1}/${this.options.choices.length}` : 'no options',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const lines = [this.theme.bold(fit(this.options.title, width)), ''];\n if (this.options.choices.length === 0) {\n lines.push(this.theme.fg(DIM, EMPTY_CHOICES));\n return lines.slice(0, height);\n }\n\n lines.push(this.theme.fg(DIM, CHOOSE_LABEL));\n // The window follows the cursor, so a list longer than the body still scrolls.\n const capacity = Math.max(1, height - HEADING_ROWS - 1);\n const start = Math.max(\n 0,\n Math.min(this.selected - capacity + 1, Math.max(0, this.options.choices.length - capacity)),\n );\n for (const [offset, choice] of this.options.choices.slice(start, start + capacity).entries()) {\n const current = start + offset === this.selected;\n const marker = current ? this.theme.fg(ACCENT, SELECTION_MARKER) : ' ';\n const label = current ? this.theme.bold(choice) : choice;\n const note = choice === this.options.preselect ? this.theme.fg(DIM, DEFAULT_MARKER) : '';\n const background: ThemeBg = current ? SELECTED_BG : ROW_BG;\n lines.push(this.theme.bg(background, rightAligned(`${marker} ${label}`, note, width)));\n }\n return lines.slice(0, height);\n }\n}\n\nexport class WorkflowInputComponent extends DoomOverlay {\n private draft = '';\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: WorkflowInputOptions,\n private readonly done: (value: string | undefined) => void,\n ) {\n super(tui, theme);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, 'enter')) {\n // An empty field commits the fallback rather than an empty string: the\n // caller offered a default, and pressing enter is how you accept it.\n this.done(this.draft.trim() || this.options.fallback || '');\n return;\n }\n if (matchesKey(data, 'backspace')) this.draft = this.draft.slice(0, -1);\n else if (data === KEY_CLEAR) this.draft = '';\n else if (!isControlInput(data)) this.draft += data;\n else return;\n this.tui.requestRender();\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: CHOICE_TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.options.breadcrumb,\n headerRight: this.options.fallback ? `default ${this.options.fallback}` : 'no default',\n footer: 'type to edit · enter confirm · ctrl+u clear · esc cancel',\n footerHints: INPUT_HINTS,\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const label = this.options.fallback\n ? rightAligned(\n this.theme.fg(DIM, VALUE_LABEL),\n this.theme.fg(DIM, `${DEFAULT_MARKER} ${this.options.fallback}`),\n width,\n )\n : this.theme.fg(DIM, VALUE_LABEL);\n return [\n this.theme.bold(fit(this.options.title, width)),\n '',\n label,\n this.theme.bg(SELECTED_BG, fit(` ${this.draft}${CURSOR_BLOCK}`, width)),\n ].slice(0, height);\n }\n}\n\nexport async function openWorkflowChoice(\n ctx: ExtensionContext,\n options: WorkflowChoiceOptions,\n): Promise<string | undefined> {\n return ctx.ui.custom<string | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowChoiceComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n\nexport async function openWorkflowInput(\n ctx: ExtensionContext,\n options: WorkflowInputOptions,\n): Promise<string | undefined> {\n return ctx.ui.custom<string | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowInputComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
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,rightAligned as a}from"./overlayText.mjs";import{matchesKey as o}from"@earendil-works/pi-tui";e.overlayOptions;const s=`WORKFLOW PROMPT`,c=`VALUE`,l=`default`,u=[[`↑↓`,`select`],[`enter`,`confirm`],[`esc`,`cancel`]],d=[[`type`,`edit`],[`enter`,`confirm`],[`ctrl+u`,`clear`],[`esc`,`cancel`]],f=`selectedBg`;var p=class extends n{options;done;selected;constructor(e,t,n,r){super(e,t),this.options=n,this.done=r,this.selected=Math.max(0,n.preselect?n.choices.indexOf(n.preselect):0)}handleInput(e){if(o(e,`escape`)||o(e,`ctrl+c`)){this.done(void 0);return}if(o(e,`enter`)){let e=this.options.choices[this.selected];e!==void 0&&this.done(e);return}let t=this.options.choices.length;if(t!==0){if(o(e,`up`)||o(e,`ctrl+p`))this.selected=Math.max(0,this.selected-1);else if(o(e,`down`)||o(e,`ctrl+n`))this.selected=Math.min(t-1,this.selected+1);else return;this.tui.requestRender()}}getChrome(){return{title:s,accent:t,breadcrumb:this.options.breadcrumb,headerRight:`${this.options.choices.length} options`,footer:`↑↓ select · enter confirm · esc cancel`,footerHints:u,footerRight:this.options.choices.length>0?`${this.selected+1}/${this.options.choices.length}`:`no options`}}renderBody(e,t){let n=[this.theme.bold(r(this.options.title,e)),``];if(this.options.choices.length===0)return n.push(this.theme.fg(`dim`,`No options are available.`)),n.slice(0,t);n.push(this.theme.fg(`dim`,`CHOOSE`));let i=Math.max(1,t-2-1),o=Math.max(0,Math.min(this.selected-i+1,Math.max(0,this.options.choices.length-i)));for(let[t,r]of this.options.choices.slice(o,o+i).entries()){let i=o+t===this.selected,s=i?this.theme.fg(`accent`,`›`):` `,c=i?this.theme.bold(r):r,u=r===this.options.preselect?this.theme.fg(`dim`,l):``,d=i?f:`userMessageBg`;n.push(this.theme.bg(d,a(`${s} ${c}`,u,e)))}return n.slice(0,t)}},m=class extends n{options;done;draft=``;constructor(e,t,n,r){super(e,t),this.options=n,this.done=r}handleInput(e){if(o(e,`escape`)||o(e,`ctrl+c`)){this.done(void 0);return}if(o(e,`enter`)){this.done(this.draft.trim()||this.options.fallback||``);return}if(o(e,`backspace`))this.draft=this.draft.slice(0,-1);else if(e===``)this.draft=``;else if(!i(e))this.draft+=e;else return;this.tui.requestRender()}getChrome(){return{title:s,accent:t,breadcrumb:this.options.breadcrumb,headerRight:this.options.fallback?`default ${this.options.fallback}`:`no default`,footer:`type to edit · enter confirm · ctrl+u clear · esc cancel`,footerHints:d}}renderBody(e,t){let n=this.options.fallback?a(this.theme.fg(`dim`,c),this.theme.fg(`dim`,`${l} ${this.options.fallback}`),e):this.theme.fg(`dim`,c);return[this.theme.bold(r(this.options.title,e)),``,n,this.theme.bg(f,r(` ${this.draft}█`,e))].slice(0,t)}};async function h(t,n){return t.ui.custom((e,t,r,i)=>new p(e,t,n,i),e)}async function g(t,n){return t.ui.custom((e,t,r,i)=>new m(e,t,n,i),e)}export{p as WorkflowChoiceComponent,m as WorkflowInputComponent,h as openWorkflowChoice,g as openWorkflowInput};
2
+ //# sourceMappingURL=workflowPrompt.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowPrompt.mjs","names":[],"sources":["../../src/workflow/workflowPrompt.ts"],"sourcesContent":["/**\n * The short prompts: pick one of a few options, or type one value.\n *\n * These were `ui.select` and `ui.input`, which meant the run manager, the runner\n * picker and every `workflow_dispatch` input rendered as native Pi widgets in the\n * middle of a flow whose other steps are framed doom overlays. Same surface for\n * all of them now, so a launch reads as one sequence rather than three styles.\n *\n * Rendering is pure, so both modes can be asserted as text without a terminal.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport {\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n DOOM_FULLSCREEN_UI_OPTIONS,\n} from './doomOverlay.ts';\nimport { matchesKey } from '@earendil-works/pi-tui';\nimport { CURSOR_BLOCK, fit, isControlInput, rightAligned, SELECTION_MARKER } from './overlayText';\n\nexport const WORKFLOW_PROMPT_OVERLAY_OPTIONS = DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions;\n\nconst CHOICE_TITLE = 'WORKFLOW PROMPT';\nconst CHOOSE_LABEL = 'CHOOSE';\nconst VALUE_LABEL = 'VALUE';\nconst DEFAULT_MARKER = 'default';\nconst EMPTY_CHOICES = 'No options are available.';\nconst CHOICE_HINTS: readonly (readonly [string, string])[] = [\n ['↑↓', 'select'],\n ['enter', 'confirm'],\n ['esc', 'cancel'],\n];\nconst INPUT_HINTS: readonly (readonly [string, string])[] = [\n ['type', 'edit'],\n ['enter', 'confirm'],\n ['ctrl+u', 'clear'],\n ['esc', 'cancel'],\n];\nconst KEY_CLEAR = '\\x15';\n/** Rows the heading and its spacer take before the first option. */\nconst HEADING_ROWS = 2;\n\n/** Neither palette name is exported, so take both from the theme's methods. */\ntype ThemeBg = Parameters<Theme['bg']>[0];\ntype ThemeColor = Parameters<Theme['fg']>[0];\n\nconst DIM: ThemeColor = 'dim';\nconst ACCENT: ThemeColor = 'accent';\nconst SELECTED_BG: ThemeBg = 'selectedBg';\nconst ROW_BG: ThemeBg = 'userMessageBg';\n\nexport interface WorkflowChoiceOptions {\n title: string;\n breadcrumb: string;\n choices: readonly string[];\n /** Marked `default` in the list and pre-selected. */\n preselect?: string;\n}\n\nexport interface WorkflowInputOptions {\n title: string;\n breadcrumb: string;\n /** Committed when the field is left empty, and shown beside the label. */\n fallback?: string;\n}\n\nexport class WorkflowChoiceComponent extends DoomOverlay {\n private selected: number;\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: WorkflowChoiceOptions,\n private readonly done: (choice: string | undefined) => void,\n ) {\n super(tui, theme);\n this.selected = Math.max(0, options.preselect ? options.choices.indexOf(options.preselect) : 0);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, 'enter')) {\n const choice = this.options.choices[this.selected];\n if (choice !== undefined) this.done(choice);\n return;\n }\n const count = this.options.choices.length;\n if (count === 0) return;\n if (matchesKey(data, 'up') || matchesKey(data, 'ctrl+p')) this.selected = Math.max(0, this.selected - 1);\n else if (matchesKey(data, 'down') || matchesKey(data, 'ctrl+n'))\n this.selected = Math.min(count - 1, this.selected + 1);\n else return;\n this.tui.requestRender();\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: CHOICE_TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.options.breadcrumb,\n headerRight: `${this.options.choices.length} options`,\n footer: '↑↓ select · enter confirm · esc cancel',\n footerHints: CHOICE_HINTS,\n footerRight:\n this.options.choices.length > 0 ? `${this.selected + 1}/${this.options.choices.length}` : 'no options',\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const lines = [this.theme.bold(fit(this.options.title, width)), ''];\n if (this.options.choices.length === 0) {\n lines.push(this.theme.fg(DIM, EMPTY_CHOICES));\n return lines.slice(0, height);\n }\n\n lines.push(this.theme.fg(DIM, CHOOSE_LABEL));\n // The window follows the cursor, so a list longer than the body still scrolls.\n const capacity = Math.max(1, height - HEADING_ROWS - 1);\n const start = Math.max(\n 0,\n Math.min(this.selected - capacity + 1, Math.max(0, this.options.choices.length - capacity)),\n );\n for (const [offset, choice] of this.options.choices.slice(start, start + capacity).entries()) {\n const current = start + offset === this.selected;\n const marker = current ? this.theme.fg(ACCENT, SELECTION_MARKER) : ' ';\n const label = current ? this.theme.bold(choice) : choice;\n const note = choice === this.options.preselect ? this.theme.fg(DIM, DEFAULT_MARKER) : '';\n const background: ThemeBg = current ? SELECTED_BG : ROW_BG;\n lines.push(this.theme.bg(background, rightAligned(`${marker} ${label}`, note, width)));\n }\n return lines.slice(0, height);\n }\n}\n\nexport class WorkflowInputComponent extends DoomOverlay {\n private draft = '';\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: WorkflowInputOptions,\n private readonly done: (value: string | undefined) => void,\n ) {\n super(tui, theme);\n }\n\n handleInput(data: string): void {\n if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) {\n this.done(undefined);\n return;\n }\n if (matchesKey(data, 'enter')) {\n // An empty field commits the fallback rather than an empty string: the\n // caller offered a default, and pressing enter is how you accept it.\n this.done(this.draft.trim() || this.options.fallback || '');\n return;\n }\n if (matchesKey(data, 'backspace')) this.draft = this.draft.slice(0, -1);\n else if (data === KEY_CLEAR) this.draft = '';\n else if (!isControlInput(data)) this.draft += data;\n else return;\n this.tui.requestRender();\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: CHOICE_TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.options.breadcrumb,\n headerRight: this.options.fallback ? `default ${this.options.fallback}` : 'no default',\n footer: 'type to edit · enter confirm · ctrl+u clear · esc cancel',\n footerHints: INPUT_HINTS,\n };\n }\n\n protected renderBody(width: number, height: number): string[] {\n const label = this.options.fallback\n ? rightAligned(\n this.theme.fg(DIM, VALUE_LABEL),\n this.theme.fg(DIM, `${DEFAULT_MARKER} ${this.options.fallback}`),\n width,\n )\n : this.theme.fg(DIM, VALUE_LABEL);\n return [\n this.theme.bold(fit(this.options.title, width)),\n '',\n label,\n this.theme.bg(SELECTED_BG, fit(` ${this.draft}${CURSOR_BLOCK}`, width)),\n ].slice(0, height);\n }\n}\n\nexport async function openWorkflowChoice(\n ctx: ExtensionContext,\n options: WorkflowChoiceOptions,\n): Promise<string | undefined> {\n return ctx.ui.custom<string | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowChoiceComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n\nexport async function openWorkflowInput(\n ctx: ExtensionContext,\n options: WorkflowInputOptions,\n): Promise<string | undefined> {\n return ctx.ui.custom<string | undefined>(\n (tui, theme, _keybindings, done) => new WorkflowInputComponent(tui, theme, options, done),\n DOOM_FULLSCREEN_UI_OPTIONS,\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=require("./doomOverlay.cjs"),t=require("./overlayText.cjs"),n=require("./workflowOverlay.cjs");let r=require("@earendil-works/pi-tui");var i=class extends e.DoomOverlay{options;escapes=new n.DoubleEscapeDetector;constructor(e,t,n){super(e,t),this.options=n}handleInput(e){if((0,r.matchesKey)(e,this.options.unfollowShortcut)){this.options.onUnfollow();return}if((0,r.matchesKey)(e,this.options.closeShortcut)){this.options.onClose();return}if(this.escapes.observe(e)===`close`){this.options.flushInput(),this.options.onClose();return}this.options.sendInput(e)}getChrome(){return{title:`WORKFLOW RUN`,accent:e.DOOM_OVERLAY_ACCENT,breadcrumb:this.options.breadcrumb,headerRight:this.options.interactive?this.options.label:`${this.options.label} · view only · this run is hosted natively`,footer:this.options.footer,footerHints:this.options.hints,footerRight:this.options.interactive?`typing goes to the run`:`typing stays here`}}renderBody(e,n){let{progress:r,output:i}=this.options.snapshot(e),a=r.slice(0,n).map(n=>t.fit(n,e)),o=n-a.length;if(o<=2)return a.slice(0,n);let s=o-2,c=i.slice(-s).map(n=>t.fit(n,e));return[...a,``,this.theme.fg(`dim`,`OUTPUT · newest last`),...c.length>0?c:[this.theme.fg(`dim`,`Waiting for output…`)]].slice(0,n)}dispose(){this.options.onDispose()}};function a(t){return{...e.DOOM_FULLSCREEN_UI_OPTIONS,overlayOptions:{...e.DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions,...t?{}:{nonCapturing:!0}}}}exports.WorkflowRunPanelComponent=i,exports.runPanelUiOptions=a;
2
+ //# sourceMappingURL=workflowRunPanel.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowRunPanel.cjs","names":["DoomOverlay","DoubleEscapeDetector","matchesKey","DOOM_OVERLAY_ACCENT","fit","DOOM_FULLSCREEN_UI_OPTIONS"],"sources":["../../src/workflow/workflowRunPanel.ts"],"sourcesContent":["/**\n * Run panel: the live view of one run's job tree and its terminal.\n *\n * A doom overlay rather than the bare header/body/footer it used to draw, so the\n * one surface a user sits inside for minutes at a time carries the same frame,\n * breadcrumb and key legend as every other. The base class owns the chrome and\n * hands the body an exact height, which also retires the hand-rolled row\n * arithmetic that existed only to stop Pi clipping the footer.\n *\n * Key routing is the reason this is a component rather than a renderer: a\n * focused panel receives input before Pi's global dispatcher, so the view\n * controls have to be handled here or the run's TTY swallows them.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport {\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n DOOM_FULLSCREEN_UI_OPTIONS,\n} from './doomOverlay.ts';\nimport { type KeyId, matchesKey } from '@earendil-works/pi-tui';\nimport { DoubleEscapeDetector } from './workflowOverlay';\nimport { fit } from './overlayText';\n\nconst TITLE = 'WORKFLOW RUN';\nconst VIEW_ONLY = 'view only · this run is hosted natively';\nconst OUTPUT_HEADING = 'OUTPUT · newest last';\nconst WAITING = 'Waiting for output…';\n/** A divider and its heading earn their rows only when output sits beneath them. */\nconst OUTPUT_CHROME_ROWS = 2;\n\nexport interface WorkflowRunPanelSnapshot {\n /** Job tree lines, already themed by the caller. */\n progress: string[];\n /** Recent terminal output for the running step. */\n output: string[];\n}\n\nexport interface WorkflowRunPanelOptions {\n runKey: string;\n /** Full run label for the header, e.g. `name · workspace/runKey`. */\n label: string;\n breadcrumb: string;\n /** False for a natively hosted run, which has no terminal to type into. */\n interactive: boolean;\n /** Plain-sentence footer, used when the terminal cannot fit the key caps. */\n footer: string;\n /** Key legend, which differs between a typeable panel and a view. */\n hints: readonly (readonly [string, string])[];\n /** Given the body width, because the job tree is fitted as it is built. */\n snapshot: (width: number) => WorkflowRunPanelSnapshot;\n /** Hand typing back to the session, leaving the panel open. */\n onUnfollow: () => void;\n onClose: () => void;\n /** Forward a keystroke to the run. */\n sendInput: (data: string) => void;\n /**\n * Send whatever is buffered before the panel goes away, or the Escape that\n * closed it dies with the batcher.\n */\n flushInput: () => void;\n unfollowShortcut: KeyId;\n closeShortcut: KeyId;\n onDispose: () => void;\n}\n\nexport class WorkflowRunPanelComponent extends DoomOverlay {\n private readonly escapes = new DoubleEscapeDetector();\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: WorkflowRunPanelOptions,\n ) {\n super(tui, theme);\n }\n\n /**\n * The chords are matched against the whole chunk, as Pi delivers one per key\n * event. A chord buried in a coalesced burst therefore misses, which is\n * acceptable now that double-Escape guarantees an exit: scanning for the bytes\n * anywhere in a chunk would instead eat legitimate input, since `\\x1b\\x17` is\n * also a valid Escape-then-ctrl+w for the run's own editor.\n */\n handleInput(data: string): void {\n if (matchesKey(data, this.options.unfollowShortcut)) {\n this.options.onUnfollow();\n return;\n }\n if (matchesKey(data, this.options.closeShortcut)) {\n this.options.onClose();\n return;\n }\n // The failsafe. The first Escape falls through to the run, so interrupting\n // the step is as fast as it ever was; only the second one inside the window\n // is consumed.\n if (this.escapes.observe(data) === 'close') {\n this.options.flushInput();\n this.options.onClose();\n return;\n }\n this.options.sendInput(data);\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.options.breadcrumb,\n headerRight: this.options.interactive ? this.options.label : `${this.options.label} · ${VIEW_ONLY}`,\n footer: this.options.footer,\n footerHints: this.options.hints,\n footerRight: this.options.interactive ? 'typing goes to the run' : 'typing stays here',\n };\n }\n\n /**\n * The tree is never truncated: knowing which job is running matters more than\n * seeing another line of output, so the terminal pane absorbs whatever the\n * tree does not use.\n */\n protected renderBody(width: number, height: number): string[] {\n const { progress, output } = this.options.snapshot(width);\n const tree = progress.slice(0, height).map((line) => fit(line, width));\n\n const spare = height - tree.length;\n if (spare <= OUTPUT_CHROME_ROWS) return tree.slice(0, height);\n\n const rows = spare - OUTPUT_CHROME_ROWS;\n const tail = output.slice(-rows).map((line) => fit(line, width));\n return [\n ...tree,\n '',\n this.theme.fg('dim', OUTPUT_HEADING),\n ...(tail.length > 0 ? tail : [this.theme.fg('dim', WAITING)]),\n ].slice(0, height);\n }\n\n dispose(): void {\n this.options.onDispose();\n }\n}\n\n/**\n * A panel whose keys cannot reach anything must never take them. Pi focuses an\n * overlay on show unless told otherwise, which for a native run would swallow\n * the user's typing into a run with no terminal to receive it.\n */\nexport function runPanelUiOptions(interactive: boolean): Record<string, unknown> {\n return {\n ...DOOM_FULLSCREEN_UI_OPTIONS,\n overlayOptions: {\n ...DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions,\n ...(interactive ? {} : { nonCapturing: true }),\n },\n };\n}\n\nexport type WorkflowRunPanelHost = Pick<ExtensionContext['ui'], 'custom'>;\n"],"mappings":"+IAoEA,IAAa,EAAb,cAA+CA,EAAAA,WAAY,CAMtC,QALnB,QAA2B,IAAIC,EAAAA,qBAE/B,YACE,EACA,EACA,EACA,CACA,MAAM,EAAK,CAAK,EAFC,KAAA,QAAA,CAGnB,CASA,YAAY,EAAoB,CAC9B,IAAA,EAAIC,EAAAA,WAAAA,CAAW,EAAM,KAAK,QAAQ,gBAAgB,EAAG,CACnD,KAAK,QAAQ,WAAW,EACxB,MACF,CACA,IAAA,EAAIA,EAAAA,WAAAA,CAAW,EAAM,KAAK,QAAQ,aAAa,EAAG,CAChD,KAAK,QAAQ,QAAQ,EACrB,MACF,CAIA,GAAI,KAAK,QAAQ,QAAQ,CAAI,IAAM,QAAS,CAC1C,KAAK,QAAQ,WAAW,EACxB,KAAK,QAAQ,QAAQ,EACrB,MACF,CACA,KAAK,QAAQ,UAAU,CAAI,CAC7B,CAEA,WAAyC,CACvC,MAAO,CACL,MAAO,eACP,OAAQC,EAAAA,oBACR,WAAY,KAAK,QAAQ,WACzB,YAAa,KAAK,QAAQ,YAAc,KAAK,QAAQ,MAAQ,GAAG,KAAK,QAAQ,MAAM,4CACnF,OAAQ,KAAK,QAAQ,OACrB,YAAa,KAAK,QAAQ,MAC1B,YAAa,KAAK,QAAQ,YAAc,yBAA2B,mBACrE,CACF,CAOA,WAAqB,EAAe,EAA0B,CAC5D,GAAM,CAAE,WAAU,UAAW,KAAK,QAAQ,SAAS,CAAK,EAClD,EAAO,EAAS,MAAM,EAAG,CAAM,CAAC,CAAC,IAAK,GAASC,EAAAA,IAAI,EAAM,CAAK,CAAC,EAE/D,EAAQ,EAAS,EAAK,OAC5B,GAAI,GAAS,EAAoB,OAAO,EAAK,MAAM,EAAG,CAAM,EAE5D,IAAM,EAAO,EAAQ,EACf,EAAO,EAAO,MAAM,CAAC,CAAI,CAAC,CAAC,IAAK,GAASA,EAAAA,IAAI,EAAM,CAAK,CAAC,EAC/D,MAAO,CACL,GAAG,EACH,GACA,KAAK,MAAM,GAAG,MAAO,sBAAc,EACnC,GAAI,EAAK,OAAS,EAAI,EAAO,CAAC,KAAK,MAAM,GAAG,MAAO,qBAAO,CAAC,CAC7D,CAAC,CAAC,MAAM,EAAG,CAAM,CACnB,CAEA,SAAgB,CACd,KAAK,QAAQ,UAAU,CACzB,CACF,EAOA,SAAgB,EAAkB,EAA+C,CAC/E,MAAO,CACL,GAAGC,EAAAA,2BACH,eAAgB,CACd,GAAGA,EAAAA,2BAA2B,eAC9B,GAAI,EAAc,CAAC,EAAI,CAAE,aAAc,EAAK,CAC9C,CACF,CACF"}
@@ -0,0 +1,2 @@
1
+ import{DOOM_FULLSCREEN_UI_OPTIONS as e,DOOM_OVERLAY_ACCENT as t,DoomOverlay as n}from"./doomOverlay.mjs";import{fit as r}from"./overlayText.mjs";import{DoubleEscapeDetector as i}from"./workflowOverlay.mjs";import{matchesKey as a}from"@earendil-works/pi-tui";var o=class extends n{options;escapes=new i;constructor(e,t,n){super(e,t),this.options=n}handleInput(e){if(a(e,this.options.unfollowShortcut)){this.options.onUnfollow();return}if(a(e,this.options.closeShortcut)){this.options.onClose();return}if(this.escapes.observe(e)===`close`){this.options.flushInput(),this.options.onClose();return}this.options.sendInput(e)}getChrome(){return{title:`WORKFLOW RUN`,accent:t,breadcrumb:this.options.breadcrumb,headerRight:this.options.interactive?this.options.label:`${this.options.label} · view only · this run is hosted natively`,footer:this.options.footer,footerHints:this.options.hints,footerRight:this.options.interactive?`typing goes to the run`:`typing stays here`}}renderBody(e,t){let{progress:n,output:i}=this.options.snapshot(e),a=n.slice(0,t).map(t=>r(t,e)),o=t-a.length;if(o<=2)return a.slice(0,t);let s=o-2,c=i.slice(-s).map(t=>r(t,e));return[...a,``,this.theme.fg(`dim`,`OUTPUT · newest last`),...c.length>0?c:[this.theme.fg(`dim`,`Waiting for output…`)]].slice(0,t)}dispose(){this.options.onDispose()}};function s(t){return{...e,overlayOptions:{...e.overlayOptions,...t?{}:{nonCapturing:!0}}}}export{o as WorkflowRunPanelComponent,s as runPanelUiOptions};
2
+ //# sourceMappingURL=workflowRunPanel.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowRunPanel.mjs","names":[],"sources":["../../src/workflow/workflowRunPanel.ts"],"sourcesContent":["/**\n * Run panel: the live view of one run's job tree and its terminal.\n *\n * A doom overlay rather than the bare header/body/footer it used to draw, so the\n * one surface a user sits inside for minutes at a time carries the same frame,\n * breadcrumb and key legend as every other. The base class owns the chrome and\n * hands the body an exact height, which also retires the hand-rolled row\n * arithmetic that existed only to stop Pi clipping the footer.\n *\n * Key routing is the reason this is a component rather than a renderer: a\n * focused panel receives input before Pi's global dispatcher, so the view\n * controls have to be handled here or the run's TTY swallows them.\n */\n\nimport type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';\nimport {\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n DOOM_FULLSCREEN_UI_OPTIONS,\n} from './doomOverlay.ts';\nimport { type KeyId, matchesKey } from '@earendil-works/pi-tui';\nimport { DoubleEscapeDetector } from './workflowOverlay';\nimport { fit } from './overlayText';\n\nconst TITLE = 'WORKFLOW RUN';\nconst VIEW_ONLY = 'view only · this run is hosted natively';\nconst OUTPUT_HEADING = 'OUTPUT · newest last';\nconst WAITING = 'Waiting for output…';\n/** A divider and its heading earn their rows only when output sits beneath them. */\nconst OUTPUT_CHROME_ROWS = 2;\n\nexport interface WorkflowRunPanelSnapshot {\n /** Job tree lines, already themed by the caller. */\n progress: string[];\n /** Recent terminal output for the running step. */\n output: string[];\n}\n\nexport interface WorkflowRunPanelOptions {\n runKey: string;\n /** Full run label for the header, e.g. `name · workspace/runKey`. */\n label: string;\n breadcrumb: string;\n /** False for a natively hosted run, which has no terminal to type into. */\n interactive: boolean;\n /** Plain-sentence footer, used when the terminal cannot fit the key caps. */\n footer: string;\n /** Key legend, which differs between a typeable panel and a view. */\n hints: readonly (readonly [string, string])[];\n /** Given the body width, because the job tree is fitted as it is built. */\n snapshot: (width: number) => WorkflowRunPanelSnapshot;\n /** Hand typing back to the session, leaving the panel open. */\n onUnfollow: () => void;\n onClose: () => void;\n /** Forward a keystroke to the run. */\n sendInput: (data: string) => void;\n /**\n * Send whatever is buffered before the panel goes away, or the Escape that\n * closed it dies with the batcher.\n */\n flushInput: () => void;\n unfollowShortcut: KeyId;\n closeShortcut: KeyId;\n onDispose: () => void;\n}\n\nexport class WorkflowRunPanelComponent extends DoomOverlay {\n private readonly escapes = new DoubleEscapeDetector();\n\n constructor(\n tui: DoomOverlayTui,\n theme: Theme,\n private readonly options: WorkflowRunPanelOptions,\n ) {\n super(tui, theme);\n }\n\n /**\n * The chords are matched against the whole chunk, as Pi delivers one per key\n * event. A chord buried in a coalesced burst therefore misses, which is\n * acceptable now that double-Escape guarantees an exit: scanning for the bytes\n * anywhere in a chunk would instead eat legitimate input, since `\\x1b\\x17` is\n * also a valid Escape-then-ctrl+w for the run's own editor.\n */\n handleInput(data: string): void {\n if (matchesKey(data, this.options.unfollowShortcut)) {\n this.options.onUnfollow();\n return;\n }\n if (matchesKey(data, this.options.closeShortcut)) {\n this.options.onClose();\n return;\n }\n // The failsafe. The first Escape falls through to the run, so interrupting\n // the step is as fast as it ever was; only the second one inside the window\n // is consumed.\n if (this.escapes.observe(data) === 'close') {\n this.options.flushInput();\n this.options.onClose();\n return;\n }\n this.options.sendInput(data);\n }\n\n protected getChrome(): DoomOverlayChrome {\n return {\n title: TITLE,\n accent: DOOM_OVERLAY_ACCENT,\n breadcrumb: this.options.breadcrumb,\n headerRight: this.options.interactive ? this.options.label : `${this.options.label} · ${VIEW_ONLY}`,\n footer: this.options.footer,\n footerHints: this.options.hints,\n footerRight: this.options.interactive ? 'typing goes to the run' : 'typing stays here',\n };\n }\n\n /**\n * The tree is never truncated: knowing which job is running matters more than\n * seeing another line of output, so the terminal pane absorbs whatever the\n * tree does not use.\n */\n protected renderBody(width: number, height: number): string[] {\n const { progress, output } = this.options.snapshot(width);\n const tree = progress.slice(0, height).map((line) => fit(line, width));\n\n const spare = height - tree.length;\n if (spare <= OUTPUT_CHROME_ROWS) return tree.slice(0, height);\n\n const rows = spare - OUTPUT_CHROME_ROWS;\n const tail = output.slice(-rows).map((line) => fit(line, width));\n return [\n ...tree,\n '',\n this.theme.fg('dim', OUTPUT_HEADING),\n ...(tail.length > 0 ? tail : [this.theme.fg('dim', WAITING)]),\n ].slice(0, height);\n }\n\n dispose(): void {\n this.options.onDispose();\n }\n}\n\n/**\n * A panel whose keys cannot reach anything must never take them. Pi focuses an\n * overlay on show unless told otherwise, which for a native run would swallow\n * the user's typing into a run with no terminal to receive it.\n */\nexport function runPanelUiOptions(interactive: boolean): Record<string, unknown> {\n return {\n ...DOOM_FULLSCREEN_UI_OPTIONS,\n overlayOptions: {\n ...DOOM_FULLSCREEN_UI_OPTIONS.overlayOptions,\n ...(interactive ? {} : { nonCapturing: true }),\n },\n };\n}\n\nexport type WorkflowRunPanelHost = Pick<ExtensionContext['ui'], 'custom'>;\n"],"mappings":"kQAoEA,IAAa,EAAb,cAA+C,CAAY,CAMtC,QALnB,QAA2B,IAAI,EAE/B,YACE,EACA,EACA,EACA,CACA,MAAM,EAAK,CAAK,EAFC,KAAA,QAAA,CAGnB,CASA,YAAY,EAAoB,CAC9B,GAAI,EAAW,EAAM,KAAK,QAAQ,gBAAgB,EAAG,CACnD,KAAK,QAAQ,WAAW,EACxB,MACF,CACA,GAAI,EAAW,EAAM,KAAK,QAAQ,aAAa,EAAG,CAChD,KAAK,QAAQ,QAAQ,EACrB,MACF,CAIA,GAAI,KAAK,QAAQ,QAAQ,CAAI,IAAM,QAAS,CAC1C,KAAK,QAAQ,WAAW,EACxB,KAAK,QAAQ,QAAQ,EACrB,MACF,CACA,KAAK,QAAQ,UAAU,CAAI,CAC7B,CAEA,WAAyC,CACvC,MAAO,CACL,MAAO,eACP,OAAQ,EACR,WAAY,KAAK,QAAQ,WACzB,YAAa,KAAK,QAAQ,YAAc,KAAK,QAAQ,MAAQ,GAAG,KAAK,QAAQ,MAAM,4CACnF,OAAQ,KAAK,QAAQ,OACrB,YAAa,KAAK,QAAQ,MAC1B,YAAa,KAAK,QAAQ,YAAc,yBAA2B,mBACrE,CACF,CAOA,WAAqB,EAAe,EAA0B,CAC5D,GAAM,CAAE,WAAU,UAAW,KAAK,QAAQ,SAAS,CAAK,EAClD,EAAO,EAAS,MAAM,EAAG,CAAM,CAAC,CAAC,IAAK,GAAS,EAAI,EAAM,CAAK,CAAC,EAE/D,EAAQ,EAAS,EAAK,OAC5B,GAAI,GAAS,EAAoB,OAAO,EAAK,MAAM,EAAG,CAAM,EAE5D,IAAM,EAAO,EAAQ,EACf,EAAO,EAAO,MAAM,CAAC,CAAI,CAAC,CAAC,IAAK,GAAS,EAAI,EAAM,CAAK,CAAC,EAC/D,MAAO,CACL,GAAG,EACH,GACA,KAAK,MAAM,GAAG,MAAO,sBAAc,EACnC,GAAI,EAAK,OAAS,EAAI,EAAO,CAAC,KAAK,MAAM,GAAG,MAAO,qBAAO,CAAC,CAC7D,CAAC,CAAC,MAAM,EAAG,CAAM,CACnB,CAEA,SAAgB,CACd,KAAK,QAAQ,UAAU,CACzB,CACF,EAOA,SAAgB,EAAkB,EAA+C,CAC/E,MAAO,CACL,GAAG,EACH,eAAgB,CACd,GAAG,EAA2B,eAC9B,GAAI,EAAc,CAAC,EAAI,CAAE,aAAc,EAAK,CAC9C,CACF,CACF"}
@@ -0,0 +1,2 @@
1
+ function e(e){return e.length<=48?e:`${e.slice(0,47)}…`}function t(e){if(!Number.isFinite(e)||e<0)return`?`;if(e<1e3)return`${e}ms`;let t=Math.round(e/1e3);if(t<60)return`${t}s`;let n=Math.floor(t/60);return n<60?`${n}m${String(t%60).padStart(2,`0`)}s`:`${Math.floor(n/60)}h${String(n%60).padStart(2,`0`)}m`}function n(n,r){let i=new Map,a=[];for(let o of r){if(o.type!==`step`||!o.step)continue;let r=`${`${n}/${o.job}`}/${o.step}`,s=e(o.step);if(o.status===`running`){i.set(r,o.at),a.push({job:o.job,key:`started:${r}:${o.at}`,status:`STARTED`,step:s});continue}let c=i.get(r),l=c?t(Date.parse(o.at)-Date.parse(c)):`?`,u=o.status===`failed`?`FAILED`:`FINISHED`;a.push({duration:l,job:o.job,key:`${u}:${r}:${o.at}`,status:u,step:s})}return a}exports.humanizeDuration=t,exports.stepReportLines=n;
2
+ //# sourceMappingURL=workflowStatusRow.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowStatusRow.cjs","names":[],"sources":["../../src/workflow/workflowStatusRow.ts"],"sourcesContent":["/**\n * Session reporting for a run's progress log.\n *\n * DESIGN PATTERNS:\n * - Pure derivation. Takes a run's raw progress events and returns report lines,\n * so the behaviour is unit-testable without a registry or a live run.\n *\n * CODING STANDARDS:\n * - Named exports only\n * - Explicit return types on exported members\n *\n * The row renderers this file used to hold are gone: running workflows are now\n * dots in the doom footer (`workflowFooterStatus.ts`) rather than a full-width\n * row above the editor.\n */\n\nimport type { WorkflowProgressEvent } from '@agimon-ai/workflow-mcp';\n\n/** Step names come from workflow YAML and can be sentences. */\nconst MAX_STEP_LABEL = 48;\n\n/** One session line, plus the key that stops it being reported twice. */\nexport interface StepReport {\n duration?: string;\n job: string;\n key: string;\n status: 'FAILED' | 'FINISHED' | 'STARTED';\n step: string;\n}\n\nfunction truncateStep(step: string): string {\n return step.length <= MAX_STEP_LABEL ? step : `${step.slice(0, MAX_STEP_LABEL - 1)}…`;\n}\n\n/**\n * Human-scale duration.\n *\n * Rounded hard on purpose: this is read at a glance next to a step name, where\n * milliseconds are noise and \"4m12s\" answers the only question being asked.\n */\nexport function humanizeDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return '?';\n if (ms < 1000) return `${ms}ms`;\n const seconds = Math.round(ms / 1000);\n if (seconds < 60) return `${seconds}s`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m${String(seconds % 60).padStart(2, '0')}s`;\n return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;\n}\n\n/**\n * Turn a run's raw progress log into session report lines.\n *\n * Derives durations by pairing each step's start with its end rather than\n * recording a duration in the log, so nothing about the log format has to\n * change and old runs read back the same way.\n *\n * Keyed by the event's own timestamp so a caller polling the same append-only\n * file repeatedly reports each transition exactly once, however often it polls.\n */\nexport function stepReportLines(runKey: string, events: WorkflowProgressEvent[]): StepReport[] {\n const startedAt = new Map<string, string>();\n const reports: StepReport[] = [];\n\n for (const event of events) {\n if (event.type !== 'step' || !event.step) continue;\n const id = `${runKey}/${event.job}`;\n const stepKey = `${id}/${event.step}`;\n const label = truncateStep(event.step);\n\n if (event.status === 'running') {\n startedAt.set(stepKey, event.at);\n reports.push({ job: event.job, key: `started:${stepKey}:${event.at}`, status: 'STARTED', step: label });\n continue;\n }\n\n const began = startedAt.get(stepKey);\n const duration = began ? humanizeDuration(Date.parse(event.at) - Date.parse(began)) : '?';\n // A failed step did finish, but calling it FINISHED buries the one outcome\n // the reader needs to see. Same shape, honest verb.\n const status = event.status === 'failed' ? 'FAILED' : 'FINISHED';\n reports.push({ duration, job: event.job, key: `${status}:${stepKey}:${event.at}`, status, step: label });\n }\n\n return reports;\n}\n"],"mappings":"AA8BA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,QAAU,GAAiB,EAAO,GAAG,EAAK,MAAM,EAAG,EAAkB,EAAE,EACrF,CAQA,SAAgB,EAAiB,EAAoB,CACnD,GAAI,CAAC,OAAO,SAAS,CAAE,GAAK,EAAK,EAAG,MAAO,IAC3C,GAAI,EAAK,IAAM,MAAO,GAAG,EAAG,IAC5B,IAAM,EAAU,KAAK,MAAM,EAAK,GAAI,EACpC,GAAI,EAAU,GAAI,MAAO,GAAG,EAAQ,GACpC,IAAM,EAAU,KAAK,MAAM,EAAU,EAAE,EAEvC,OADI,EAAU,GAAW,GAAG,EAAQ,GAAG,OAAO,EAAU,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,EAAE,GACtE,GAAG,KAAK,MAAM,EAAU,EAAE,EAAE,GAAG,OAAO,EAAU,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,EAAE,EAC9E,CAYA,SAAgB,EAAgB,EAAgB,EAA+C,CAC7F,IAAM,EAAY,IAAI,IAChB,EAAwB,CAAC,EAE/B,IAAK,IAAM,KAAS,EAAQ,CAC1B,GAAI,EAAM,OAAS,QAAU,CAAC,EAAM,KAAM,SAE1C,IAAM,EAAU,GAAG,GADL,EAAO,GAAG,EAAM,MACR,GAAG,EAAM,OACzB,EAAQ,EAAa,EAAM,IAAI,EAErC,GAAI,EAAM,SAAW,UAAW,CAC9B,EAAU,IAAI,EAAS,EAAM,EAAE,EAC/B,EAAQ,KAAK,CAAE,IAAK,EAAM,IAAK,IAAK,WAAW,EAAQ,GAAG,EAAM,KAAM,OAAQ,UAAW,KAAM,CAAM,CAAC,EACtG,QACF,CAEA,IAAM,EAAQ,EAAU,IAAI,CAAO,EAC7B,EAAW,EAAQ,EAAiB,KAAK,MAAM,EAAM,EAAE,EAAI,KAAK,MAAM,CAAK,CAAC,EAAI,IAGhF,EAAS,EAAM,SAAW,SAAW,SAAW,WACtD,EAAQ,KAAK,CAAE,WAAU,IAAK,EAAM,IAAK,IAAK,GAAG,EAAO,GAAG,EAAQ,GAAG,EAAM,KAAM,SAAQ,KAAM,CAAM,CAAC,CACzG,CAEA,OAAO,CACT"}
@@ -0,0 +1,2 @@
1
+ function e(e){return e.length<=48?e:`${e.slice(0,47)}…`}function t(e){if(!Number.isFinite(e)||e<0)return`?`;if(e<1e3)return`${e}ms`;let t=Math.round(e/1e3);if(t<60)return`${t}s`;let n=Math.floor(t/60);return n<60?`${n}m${String(t%60).padStart(2,`0`)}s`:`${Math.floor(n/60)}h${String(n%60).padStart(2,`0`)}m`}function n(n,r){let i=new Map,a=[];for(let o of r){if(o.type!==`step`||!o.step)continue;let r=`${`${n}/${o.job}`}/${o.step}`,s=e(o.step);if(o.status===`running`){i.set(r,o.at),a.push({job:o.job,key:`started:${r}:${o.at}`,status:`STARTED`,step:s});continue}let c=i.get(r),l=c?t(Date.parse(o.at)-Date.parse(c)):`?`,u=o.status===`failed`?`FAILED`:`FINISHED`;a.push({duration:l,job:o.job,key:`${u}:${r}:${o.at}`,status:u,step:s})}return a}export{t as humanizeDuration,n as stepReportLines};
2
+ //# sourceMappingURL=workflowStatusRow.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowStatusRow.mjs","names":[],"sources":["../../src/workflow/workflowStatusRow.ts"],"sourcesContent":["/**\n * Session reporting for a run's progress log.\n *\n * DESIGN PATTERNS:\n * - Pure derivation. Takes a run's raw progress events and returns report lines,\n * so the behaviour is unit-testable without a registry or a live run.\n *\n * CODING STANDARDS:\n * - Named exports only\n * - Explicit return types on exported members\n *\n * The row renderers this file used to hold are gone: running workflows are now\n * dots in the doom footer (`workflowFooterStatus.ts`) rather than a full-width\n * row above the editor.\n */\n\nimport type { WorkflowProgressEvent } from '@agimon-ai/workflow-mcp';\n\n/** Step names come from workflow YAML and can be sentences. */\nconst MAX_STEP_LABEL = 48;\n\n/** One session line, plus the key that stops it being reported twice. */\nexport interface StepReport {\n duration?: string;\n job: string;\n key: string;\n status: 'FAILED' | 'FINISHED' | 'STARTED';\n step: string;\n}\n\nfunction truncateStep(step: string): string {\n return step.length <= MAX_STEP_LABEL ? step : `${step.slice(0, MAX_STEP_LABEL - 1)}…`;\n}\n\n/**\n * Human-scale duration.\n *\n * Rounded hard on purpose: this is read at a glance next to a step name, where\n * milliseconds are noise and \"4m12s\" answers the only question being asked.\n */\nexport function humanizeDuration(ms: number): string {\n if (!Number.isFinite(ms) || ms < 0) return '?';\n if (ms < 1000) return `${ms}ms`;\n const seconds = Math.round(ms / 1000);\n if (seconds < 60) return `${seconds}s`;\n const minutes = Math.floor(seconds / 60);\n if (minutes < 60) return `${minutes}m${String(seconds % 60).padStart(2, '0')}s`;\n return `${Math.floor(minutes / 60)}h${String(minutes % 60).padStart(2, '0')}m`;\n}\n\n/**\n * Turn a run's raw progress log into session report lines.\n *\n * Derives durations by pairing each step's start with its end rather than\n * recording a duration in the log, so nothing about the log format has to\n * change and old runs read back the same way.\n *\n * Keyed by the event's own timestamp so a caller polling the same append-only\n * file repeatedly reports each transition exactly once, however often it polls.\n */\nexport function stepReportLines(runKey: string, events: WorkflowProgressEvent[]): StepReport[] {\n const startedAt = new Map<string, string>();\n const reports: StepReport[] = [];\n\n for (const event of events) {\n if (event.type !== 'step' || !event.step) continue;\n const id = `${runKey}/${event.job}`;\n const stepKey = `${id}/${event.step}`;\n const label = truncateStep(event.step);\n\n if (event.status === 'running') {\n startedAt.set(stepKey, event.at);\n reports.push({ job: event.job, key: `started:${stepKey}:${event.at}`, status: 'STARTED', step: label });\n continue;\n }\n\n const began = startedAt.get(stepKey);\n const duration = began ? humanizeDuration(Date.parse(event.at) - Date.parse(began)) : '?';\n // A failed step did finish, but calling it FINISHED buries the one outcome\n // the reader needs to see. Same shape, honest verb.\n const status = event.status === 'failed' ? 'FAILED' : 'FINISHED';\n reports.push({ duration, job: event.job, key: `${status}:${stepKey}:${event.at}`, status, step: label });\n }\n\n return reports;\n}\n"],"mappings":"AA8BA,SAAS,EAAa,EAAsB,CAC1C,OAAO,EAAK,QAAU,GAAiB,EAAO,GAAG,EAAK,MAAM,EAAG,EAAkB,EAAE,EACrF,CAQA,SAAgB,EAAiB,EAAoB,CACnD,GAAI,CAAC,OAAO,SAAS,CAAE,GAAK,EAAK,EAAG,MAAO,IAC3C,GAAI,EAAK,IAAM,MAAO,GAAG,EAAG,IAC5B,IAAM,EAAU,KAAK,MAAM,EAAK,GAAI,EACpC,GAAI,EAAU,GAAI,MAAO,GAAG,EAAQ,GACpC,IAAM,EAAU,KAAK,MAAM,EAAU,EAAE,EAEvC,OADI,EAAU,GAAW,GAAG,EAAQ,GAAG,OAAO,EAAU,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,EAAE,GACtE,GAAG,KAAK,MAAM,EAAU,EAAE,EAAE,GAAG,OAAO,EAAU,EAAE,CAAC,CAAC,SAAS,EAAG,GAAG,EAAE,EAC9E,CAYA,SAAgB,EAAgB,EAAgB,EAA+C,CAC7F,IAAM,EAAY,IAAI,IAChB,EAAwB,CAAC,EAE/B,IAAK,IAAM,KAAS,EAAQ,CAC1B,GAAI,EAAM,OAAS,QAAU,CAAC,EAAM,KAAM,SAE1C,IAAM,EAAU,GAAG,GADL,EAAO,GAAG,EAAM,MACR,GAAG,EAAM,OACzB,EAAQ,EAAa,EAAM,IAAI,EAErC,GAAI,EAAM,SAAW,UAAW,CAC9B,EAAU,IAAI,EAAS,EAAM,EAAE,EAC/B,EAAQ,KAAK,CAAE,IAAK,EAAM,IAAK,IAAK,WAAW,EAAQ,GAAG,EAAM,KAAM,OAAQ,UAAW,KAAM,CAAM,CAAC,EACtG,QACF,CAEA,IAAM,EAAQ,EAAU,IAAI,CAAO,EAC7B,EAAW,EAAQ,EAAiB,KAAK,MAAM,EAAM,EAAE,EAAI,KAAK,MAAM,CAAK,CAAC,EAAI,IAGhF,EAAS,EAAM,SAAW,SAAW,SAAW,WACtD,EAAQ,KAAK,CAAE,WAAU,IAAK,EAAM,IAAK,IAAK,GAAG,EAAO,GAAG,EAAQ,GAAG,EAAM,KAAM,SAAQ,KAAM,CAAM,CAAC,CACzG,CAEA,OAAO,CACT"}
@@ -0,0 +1,2 @@
1
+ let e=require("@earendil-works/pi-tui");function t(e,t){return{displayName:e,job:t.job,status:t.status,step:t.step,...t.duration?{duration:t.duration}:{}}}function n(e){let t=e.duration?`${e.status}::${e.duration}::${e.step}`:`${e.status}::${e.step}`;return`[${e.displayName}]: ${e.job}\n${t}`}function r(e){if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e;return typeof t.displayName==`string`&&typeof t.job==`string`&&typeof t.step==`string`&&(t.status===`STARTED`||t.status===`FINISHED`||t.status===`FAILED`)&&(t.duration===void 0||typeof t.duration==`string`)}var i=class{details;theme;constructor(e,t){this.details=e,this.theme=t}render(t){let n=`${this.theme.fg(`accent`,`[${this.details.displayName}]:`)} ${this.details.job}`,r=this.details.duration?`${this.details.status}::${this.details.duration}::${this.details.step}`:`${this.details.status}::${this.details.step}`,i=this.details.status===`FAILED`?`error`:this.details.status===`FINISHED`?`success`:`accent`;return[(0,e.truncateToWidth)(n,t),(0,e.truncateToWidth)(this.theme.fg(i,r),t)]}invalidate(){}};function a(t,n,r,a){let o=new e.Box(r,1,e=>a.bg(`customMessageBg`,e));return o.addChild(t?new i(t,a):new e.Text(n,0,0)),o}exports.formatWorkflowStepMessage=n,exports.isWorkflowStepMessageDetails=r,exports.renderWorkflowStepMessage=a,exports.workflowStepMessageDetails=t;
2
+ //# sourceMappingURL=workflowStepMessage.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowStepMessage.cjs","names":["truncateToWidth","Box","Text"],"sources":["../../src/workflow/workflowStepMessage.ts"],"sourcesContent":["import type { Theme } from '@earendil-works/pi-coding-agent';\nimport { Box, type Component, Text, truncateToWidth } from '@earendil-works/pi-tui';\nimport type { StepReport } from './workflowStatusRow';\n\nexport interface WorkflowStepMessageDetails {\n displayName: string;\n duration?: string;\n job: string;\n status: StepReport['status'];\n step: string;\n}\n\nexport function workflowStepMessageDetails(displayName: string, report: StepReport): WorkflowStepMessageDetails {\n return {\n displayName,\n job: report.job,\n status: report.status,\n step: report.step,\n ...(report.duration ? { duration: report.duration } : {}),\n };\n}\n\nexport function formatWorkflowStepMessage(details: WorkflowStepMessageDetails): string {\n const transition = details.duration\n ? `${details.status}::${details.duration}::${details.step}`\n : `${details.status}::${details.step}`;\n return `[${details.displayName}]: ${details.job}\\n${transition}`;\n}\n\nexport function isWorkflowStepMessageDetails(value: unknown): value is WorkflowStepMessageDetails {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const candidate = value as Partial<WorkflowStepMessageDetails>;\n return (\n typeof candidate.displayName === 'string' &&\n typeof candidate.job === 'string' &&\n typeof candidate.step === 'string' &&\n (candidate.status === 'STARTED' || candidate.status === 'FINISHED' || candidate.status === 'FAILED') &&\n (candidate.duration === undefined || typeof candidate.duration === 'string')\n );\n}\n\nclass WorkflowStepMessageLines implements Component {\n constructor(\n private readonly details: WorkflowStepMessageDetails,\n private readonly theme: Theme,\n ) {}\n\n render(width: number): string[] {\n const header = `${this.theme.fg('accent', `[${this.details.displayName}]:`)} ${this.details.job}`;\n const transition = this.details.duration\n ? `${this.details.status}::${this.details.duration}::${this.details.step}`\n : `${this.details.status}::${this.details.step}`;\n const colour =\n this.details.status === 'FAILED' ? 'error' : this.details.status === 'FINISHED' ? 'success' : 'accent';\n return [truncateToWidth(header, width), truncateToWidth(this.theme.fg(colour, transition), width)];\n }\n\n invalidate(): void {}\n}\n\nexport function renderWorkflowStepMessage(\n details: WorkflowStepMessageDetails | undefined,\n fallback: string,\n outputPad: number,\n theme: Theme,\n): Component {\n const box = new Box(outputPad, 1, (text) => theme.bg('customMessageBg', text));\n box.addChild(details ? new WorkflowStepMessageLines(details, theme) : new Text(fallback, 0, 0));\n return box;\n}\n"],"mappings":"wCAYA,SAAgB,EAA2B,EAAqB,EAAgD,CAC9G,MAAO,CACL,cACA,IAAK,EAAO,IACZ,OAAQ,EAAO,OACf,KAAM,EAAO,KACb,GAAI,EAAO,SAAW,CAAE,SAAU,EAAO,QAAS,EAAI,CAAC,CACzD,CACF,CAEA,SAAgB,EAA0B,EAA6C,CACrF,IAAM,EAAa,EAAQ,SACvB,GAAG,EAAQ,OAAO,IAAI,EAAQ,SAAS,IAAI,EAAQ,OACnD,GAAG,EAAQ,OAAO,IAAI,EAAQ,OAClC,MAAO,IAAI,EAAQ,YAAY,KAAK,EAAQ,IAAI,IAAI,GACtD,CAEA,SAAgB,EAA6B,EAAqD,CAChG,GAAI,CAAC,GAAS,OAAO,GAAU,UAAY,MAAM,QAAQ,CAAK,EAAG,MAAO,GACxE,IAAM,EAAY,EAClB,OACE,OAAO,EAAU,aAAgB,UACjC,OAAO,EAAU,KAAQ,UACzB,OAAO,EAAU,MAAS,WACzB,EAAU,SAAW,WAAa,EAAU,SAAW,YAAc,EAAU,SAAW,YAC1F,EAAU,WAAa,IAAA,IAAa,OAAO,EAAU,UAAa,SAEvE,CAEA,IAAM,EAAN,KAAoD,CAE/B,QACA,MAFnB,YACE,EACA,EACA,CAFiB,KAAA,QAAA,EACA,KAAA,MAAA,CAChB,CAEH,OAAO,EAAyB,CAC9B,IAAM,EAAS,GAAG,KAAK,MAAM,GAAG,SAAU,IAAI,KAAK,QAAQ,YAAY,GAAG,EAAE,GAAG,KAAK,QAAQ,MACtF,EAAa,KAAK,QAAQ,SAC5B,GAAG,KAAK,QAAQ,OAAO,IAAI,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,OAClE,GAAG,KAAK,QAAQ,OAAO,IAAI,KAAK,QAAQ,OACtC,EACJ,KAAK,QAAQ,SAAW,SAAW,QAAU,KAAK,QAAQ,SAAW,WAAa,UAAY,SAChG,MAAO,EAAA,EAACA,EAAAA,gBAAAA,CAAgB,EAAQ,CAAK,GAAA,EAAGA,EAAAA,gBAAAA,CAAgB,KAAK,MAAM,GAAG,EAAQ,CAAU,EAAG,CAAK,CAAC,CACnG,CAEA,YAAmB,CAAC,CACtB,EAEA,SAAgB,EACd,EACA,EACA,EACA,EACW,CACX,IAAM,EAAM,IAAIC,EAAAA,IAAI,EAAW,EAAI,GAAS,EAAM,GAAG,kBAAmB,CAAI,CAAC,EAE7E,OADA,EAAI,SAAS,EAAU,IAAI,EAAyB,EAAS,CAAK,EAAI,IAAIC,EAAAA,KAAK,EAAU,EAAG,CAAC,CAAC,EACvF,CACT"}
@@ -0,0 +1,2 @@
1
+ import{Box as e,Text as t,truncateToWidth as n}from"@earendil-works/pi-tui";function r(e,t){return{displayName:e,job:t.job,status:t.status,step:t.step,...t.duration?{duration:t.duration}:{}}}function i(e){let t=e.duration?`${e.status}::${e.duration}::${e.step}`:`${e.status}::${e.step}`;return`[${e.displayName}]: ${e.job}\n${t}`}function a(e){if(!e||typeof e!=`object`||Array.isArray(e))return!1;let t=e;return typeof t.displayName==`string`&&typeof t.job==`string`&&typeof t.step==`string`&&(t.status===`STARTED`||t.status===`FINISHED`||t.status===`FAILED`)&&(t.duration===void 0||typeof t.duration==`string`)}var o=class{details;theme;constructor(e,t){this.details=e,this.theme=t}render(e){let t=`${this.theme.fg(`accent`,`[${this.details.displayName}]:`)} ${this.details.job}`,r=this.details.duration?`${this.details.status}::${this.details.duration}::${this.details.step}`:`${this.details.status}::${this.details.step}`,i=this.details.status===`FAILED`?`error`:this.details.status===`FINISHED`?`success`:`accent`;return[n(t,e),n(this.theme.fg(i,r),e)]}invalidate(){}};function s(n,r,i,a){let s=new e(i,1,e=>a.bg(`customMessageBg`,e));return s.addChild(n?new o(n,a):new t(r,0,0)),s}export{i as formatWorkflowStepMessage,a as isWorkflowStepMessageDetails,s as renderWorkflowStepMessage,r as workflowStepMessageDetails};
2
+ //# sourceMappingURL=workflowStepMessage.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowStepMessage.mjs","names":[],"sources":["../../src/workflow/workflowStepMessage.ts"],"sourcesContent":["import type { Theme } from '@earendil-works/pi-coding-agent';\nimport { Box, type Component, Text, truncateToWidth } from '@earendil-works/pi-tui';\nimport type { StepReport } from './workflowStatusRow';\n\nexport interface WorkflowStepMessageDetails {\n displayName: string;\n duration?: string;\n job: string;\n status: StepReport['status'];\n step: string;\n}\n\nexport function workflowStepMessageDetails(displayName: string, report: StepReport): WorkflowStepMessageDetails {\n return {\n displayName,\n job: report.job,\n status: report.status,\n step: report.step,\n ...(report.duration ? { duration: report.duration } : {}),\n };\n}\n\nexport function formatWorkflowStepMessage(details: WorkflowStepMessageDetails): string {\n const transition = details.duration\n ? `${details.status}::${details.duration}::${details.step}`\n : `${details.status}::${details.step}`;\n return `[${details.displayName}]: ${details.job}\\n${transition}`;\n}\n\nexport function isWorkflowStepMessageDetails(value: unknown): value is WorkflowStepMessageDetails {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n const candidate = value as Partial<WorkflowStepMessageDetails>;\n return (\n typeof candidate.displayName === 'string' &&\n typeof candidate.job === 'string' &&\n typeof candidate.step === 'string' &&\n (candidate.status === 'STARTED' || candidate.status === 'FINISHED' || candidate.status === 'FAILED') &&\n (candidate.duration === undefined || typeof candidate.duration === 'string')\n );\n}\n\nclass WorkflowStepMessageLines implements Component {\n constructor(\n private readonly details: WorkflowStepMessageDetails,\n private readonly theme: Theme,\n ) {}\n\n render(width: number): string[] {\n const header = `${this.theme.fg('accent', `[${this.details.displayName}]:`)} ${this.details.job}`;\n const transition = this.details.duration\n ? `${this.details.status}::${this.details.duration}::${this.details.step}`\n : `${this.details.status}::${this.details.step}`;\n const colour =\n this.details.status === 'FAILED' ? 'error' : this.details.status === 'FINISHED' ? 'success' : 'accent';\n return [truncateToWidth(header, width), truncateToWidth(this.theme.fg(colour, transition), width)];\n }\n\n invalidate(): void {}\n}\n\nexport function renderWorkflowStepMessage(\n details: WorkflowStepMessageDetails | undefined,\n fallback: string,\n outputPad: number,\n theme: Theme,\n): Component {\n const box = new Box(outputPad, 1, (text) => theme.bg('customMessageBg', text));\n box.addChild(details ? new WorkflowStepMessageLines(details, theme) : new Text(fallback, 0, 0));\n return box;\n}\n"],"mappings":"4EAYA,SAAgB,EAA2B,EAAqB,EAAgD,CAC9G,MAAO,CACL,cACA,IAAK,EAAO,IACZ,OAAQ,EAAO,OACf,KAAM,EAAO,KACb,GAAI,EAAO,SAAW,CAAE,SAAU,EAAO,QAAS,EAAI,CAAC,CACzD,CACF,CAEA,SAAgB,EAA0B,EAA6C,CACrF,IAAM,EAAa,EAAQ,SACvB,GAAG,EAAQ,OAAO,IAAI,EAAQ,SAAS,IAAI,EAAQ,OACnD,GAAG,EAAQ,OAAO,IAAI,EAAQ,OAClC,MAAO,IAAI,EAAQ,YAAY,KAAK,EAAQ,IAAI,IAAI,GACtD,CAEA,SAAgB,EAA6B,EAAqD,CAChG,GAAI,CAAC,GAAS,OAAO,GAAU,UAAY,MAAM,QAAQ,CAAK,EAAG,MAAO,GACxE,IAAM,EAAY,EAClB,OACE,OAAO,EAAU,aAAgB,UACjC,OAAO,EAAU,KAAQ,UACzB,OAAO,EAAU,MAAS,WACzB,EAAU,SAAW,WAAa,EAAU,SAAW,YAAc,EAAU,SAAW,YAC1F,EAAU,WAAa,IAAA,IAAa,OAAO,EAAU,UAAa,SAEvE,CAEA,IAAM,EAAN,KAAoD,CAE/B,QACA,MAFnB,YACE,EACA,EACA,CAFiB,KAAA,QAAA,EACA,KAAA,MAAA,CAChB,CAEH,OAAO,EAAyB,CAC9B,IAAM,EAAS,GAAG,KAAK,MAAM,GAAG,SAAU,IAAI,KAAK,QAAQ,YAAY,GAAG,EAAE,GAAG,KAAK,QAAQ,MACtF,EAAa,KAAK,QAAQ,SAC5B,GAAG,KAAK,QAAQ,OAAO,IAAI,KAAK,QAAQ,SAAS,IAAI,KAAK,QAAQ,OAClE,GAAG,KAAK,QAAQ,OAAO,IAAI,KAAK,QAAQ,OACtC,EACJ,KAAK,QAAQ,SAAW,SAAW,QAAU,KAAK,QAAQ,SAAW,WAAa,UAAY,SAChG,MAAO,CAAC,EAAgB,EAAQ,CAAK,EAAG,EAAgB,KAAK,MAAM,GAAG,EAAQ,CAAU,EAAG,CAAK,CAAC,CACnG,CAEA,YAAmB,CAAC,CACtB,EAEA,SAAgB,EACd,EACA,EACA,EACA,EACW,CACX,IAAM,EAAM,IAAI,EAAI,EAAW,EAAI,GAAS,EAAM,GAAG,kBAAmB,CAAI,CAAC,EAE7E,OADA,EAAI,SAAS,EAAU,IAAI,EAAyB,EAAS,CAAK,EAAI,IAAI,EAAK,EAAU,EAAG,CAAC,CAAC,EACvF,CACT"}
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@agimon-ai/doompi-workflow",
3
+ "version": "0.0.1-alpha.0",
4
+ "description": "Doompi workflow execution and monitoring integration",
5
+ "homepage": "https://agimon.ai",
6
+ "license": "MIT",
7
+ "author": "Vuong Ngo",
8
+ "files": [
9
+ "dist",
10
+ "skills"
11
+ ],
12
+ "type": "module",
13
+ "main": "./dist/index.cjs",
14
+ "types": "./dist/index.d.mts",
15
+ "jsnext:main": "./dist/index.mjs",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.mts",
19
+ "import": "./dist/index.mjs",
20
+ "require": "./dist/index.cjs"
21
+ },
22
+ "./extensions/dispatcher": {
23
+ "types": "./dist/extensions/dispatcher.d.mts",
24
+ "import": "./dist/extensions/dispatcher.mjs",
25
+ "require": "./dist/extensions/dispatcher.cjs"
26
+ },
27
+ "./extensions/pi": {
28
+ "types": "./dist/extensions/pi.d.mts",
29
+ "import": "./dist/extensions/pi.mjs",
30
+ "require": "./dist/extensions/pi.cjs"
31
+ },
32
+ "./package.json": "./package.json",
33
+ "./skills/workflow-recovery/SKILL.md": "./skills/workflow-recovery/SKILL.md"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "dependencies": {
39
+ "@modelcontextprotocol/sdk": "1.29.0",
40
+ "zod": "4.4.3",
41
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.0",
42
+ "@agimon-ai/doompi-telemetry": "0.0.1-alpha.0",
43
+ "@agimon-ai/workflow-mcp": "0.22.9"
44
+ },
45
+ "devDependencies": {
46
+ "@earendil-works/pi-coding-agent": "0.84.1",
47
+ "@earendil-works/pi-tui": "0.84.1",
48
+ "@types/node": "26.1.1",
49
+ "@vitest/coverage-v8": "4.1.10",
50
+ "tsdown": "0.22.14",
51
+ "typescript": "6.0.3",
52
+ "vitest": "4.1.10",
53
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.0"
54
+ },
55
+ "peerDependencies": {
56
+ "@earendil-works/pi-coding-agent": "0.84.1",
57
+ "@earendil-works/pi-tui": "0.84.1"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "@earendil-works/pi-coding-agent": {
61
+ "optional": true
62
+ },
63
+ "@earendil-works/pi-tui": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "engines": {
68
+ "node": ">=22.19.0"
69
+ },
70
+ "pi": {
71
+ "extensions": [
72
+ "./dist/extensions/pi.mjs"
73
+ ]
74
+ },
75
+ "scripts": {
76
+ "build": "tsdown",
77
+ "test": "vitest --run",
78
+ "typecheck": "tsc --noEmit",
79
+ "lint": "oxlint . && oxfmt . --check",
80
+ "fixcode": "oxlint . --fix && oxfmt ."
81
+ }
82
+ }
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: workflow-recovery
3
+ description: Recover a failed or interrupted workflow-mcp run so it resumes from its active repair rather than starting over. Use before calling workflow_run with action recover, whenever a run shows stage error or interrupted, or when deciding between recovering a run, launching a fresh one, and deferring it. Covers the evidence to gather first, the three permitted outcomes, the judgement cases, and the files that must never be edited.
4
+ ---
5
+
6
+ # Recovering a workflow run
7
+
8
+ A recovery is **not** a relaunch, and the difference is expensive to get wrong. A
9
+ relaunch throws away the run's state and starts again; a recovery resumes the
10
+ existing run from its active repair, preserving the worktree and the work already
11
+ done. Choosing the wrong one either destroys progress or resumes a run that can
12
+ never succeed.
13
+
14
+ Read this before calling `workflow_run` with action `recover`.
15
+
16
+ ## 1. Gather evidence first
17
+
18
+ Never act on the stage alone. Before touching a failed run, collect:
19
+
20
+ - **Every log source**, not just the first one that looks relevant.
21
+ - **Process state** — is anything still alive from the original run?
22
+ - **The exact staged `run.json`** for the run.
23
+ - **Only the repair referenced by `activeRepairId`.** Other repairs in the
24
+ directory belong to earlier cycles and are not yours to act on.
25
+
26
+ Use `workflow_run` with action `status` for stage and identity, and action `tail`
27
+ to read the launcher's recent output.
28
+
29
+ ## 2. Choose exactly one outcome
30
+
31
+ Every failed run resolves to one of three, and never two in the same pass:
32
+
33
+ | Outcome | When |
34
+ | ---------------------------- | ----------------------------------------------------------------------------------------- |
35
+ | **Recover** the existing run | The recorded target is still valid and the failure was transient or an interruption |
36
+ | **Launch fresh** | The run cannot resume, or the branch has diverged far enough that resuming is meaningless |
37
+ | **Defer** | A named prerequisite is materially unresolved |
38
+
39
+ Recovery and a fresh launch each consume one launch from the pass budget.
40
+
41
+ ## 3. Judgement cases
42
+
43
+ - `interrupted` with a valid failed job — the strong recovery case.
44
+ - `failed` with a valid failed job — recoverable when the failure was transient
45
+ and the recorded target remains valid.
46
+ - **Empty failed job** — cannot be recovered. Launch fresh if the work is ready,
47
+ otherwise defer.
48
+ - **Terminal repair-triage run** — must never be recovered. Launch fresh once the
49
+ work is ready again.
50
+ - **Stale branch** — prefer a fresh launch once the branch has diverged enough
51
+ that resuming would rebase onto unrecognisable history.
52
+ - **No job id** — the run was launched by hand and is outside dispatch. Report
53
+ it; do not recover it.
54
+ - **Blocked** — never recover or launch a run whose job sits in a blocked column.
55
+ A human resolves the blocker first.
56
+
57
+ ## 4. Hard constraints
58
+
59
+ - **Never edit `issue.md` or `repair.json`.** They are the record of what went
60
+ wrong; editing them destroys the evidence and the repair chain.
61
+ - **Never manufacture a recovery target.** If the recorded target is invalid,
62
+ that is a fresh-launch case, not a reason to invent one.
63
+ - Prepare `run.json` **only** when verified recovery state requires it.
64
+ - Preserve the active repair and the existing worktree identity.
65
+ - Do not use `--dry-run` for a real recovery.
66
+
67
+ ## 5. Verify before reporting
68
+
69
+ Recover through the launcher recorded for the run. Then confirm **real process
70
+ and registry progress**. A `workflow_run` action `recover` call that returns is
71
+ not a recovery that succeeded. Check `workflow_run` with action `status` before
72
+ reporting an outcome, and say what you recovered, what you skipped and why, and
73
+ anything still blocked.