@agimon-ai/doompi-workflow 0.0.1-alpha.37 → 0.0.1-alpha.39

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 (125) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +14 -8
  3. package/dist/_virtual/_rolldown/runtime.cjs +1 -0
  4. package/dist/adapters/pi/extension.cjs +1 -1
  5. package/dist/adapters/pi/extension.mjs +1 -1
  6. package/dist/adapters/pi/workflow/piExtension.cjs +6 -8
  7. package/dist/adapters/pi/workflow/piExtension.cjs.map +1 -1
  8. package/dist/adapters/pi/workflow/piExtension.mjs +6 -8
  9. package/dist/adapters/pi/workflow/piExtension.mjs.map +1 -1
  10. package/dist/adapters/pi/workflow/piTools.cjs +3 -3
  11. package/dist/adapters/pi/workflow/piTools.mjs +3 -3
  12. package/dist/adapters/pi/workflow/workflowLauncher.cjs +1 -1
  13. package/dist/adapters/pi/workflow/workflowLauncher.cjs.map +1 -1
  14. package/dist/adapters/pi/workflow/workflowLauncher.mjs +1 -1
  15. package/dist/adapters/pi/workflow/workflowLauncher.mjs.map +1 -1
  16. package/dist/adapters/workflowCatalogChannel.cjs +2 -0
  17. package/dist/adapters/workflowCatalogChannel.cjs.map +1 -0
  18. package/dist/adapters/workflowCatalogChannel.d.cts +23 -0
  19. package/dist/adapters/workflowCatalogChannel.d.cts.map +1 -0
  20. package/dist/adapters/workflowCatalogChannel.d.mts +23 -0
  21. package/dist/adapters/workflowCatalogChannel.d.mts.map +1 -0
  22. package/dist/adapters/workflowCatalogChannel.mjs +2 -0
  23. package/dist/adapters/workflowCatalogChannel.mjs.map +1 -0
  24. package/dist/adapters/workflowHubApi.cjs +2 -0
  25. package/dist/adapters/workflowHubApi.cjs.map +1 -0
  26. package/dist/adapters/workflowHubApi.d.cts +24 -0
  27. package/dist/adapters/workflowHubApi.d.cts.map +1 -0
  28. package/dist/adapters/workflowHubApi.d.mts +24 -0
  29. package/dist/adapters/workflowHubApi.d.mts.map +1 -0
  30. package/dist/adapters/workflowHubApi.mjs +2 -0
  31. package/dist/adapters/workflowHubApi.mjs.map +1 -0
  32. package/dist/adapters/workflowWatcher.cjs +2 -0
  33. package/dist/adapters/workflowWatcher.cjs.map +1 -0
  34. package/dist/adapters/workflowWatcher.d.cts +24 -0
  35. package/dist/adapters/workflowWatcher.d.cts.map +1 -0
  36. package/dist/adapters/workflowWatcher.d.mts +24 -0
  37. package/dist/adapters/workflowWatcher.d.mts.map +1 -0
  38. package/dist/adapters/workflowWatcher.mjs +2 -0
  39. package/dist/adapters/workflowWatcher.mjs.map +1 -0
  40. package/dist/adapters/workflowsHubChannel.cjs +2 -0
  41. package/dist/adapters/workflowsHubChannel.cjs.map +1 -0
  42. package/dist/adapters/workflowsHubChannel.d.cts +20 -0
  43. package/dist/adapters/workflowsHubChannel.d.cts.map +1 -0
  44. package/dist/adapters/workflowsHubChannel.d.mts +20 -0
  45. package/dist/adapters/workflowsHubChannel.d.mts.map +1 -0
  46. package/dist/adapters/workflowsHubChannel.mjs +2 -0
  47. package/dist/adapters/workflowsHubChannel.mjs.map +1 -0
  48. package/dist/hubApi.cjs +1 -0
  49. package/dist/hubApi.d.cts +2 -0
  50. package/dist/hubApi.d.mts +2 -0
  51. package/dist/hubApi.mjs +1 -0
  52. package/dist/schemas/workflowPi.cjs +1 -1
  53. package/dist/schemas/workflowPi.cjs.map +1 -1
  54. package/dist/schemas/workflowPi.mjs +1 -1
  55. package/dist/schemas/workflowPi.mjs.map +1 -1
  56. package/dist/services/webWorkflowCatalog.cjs +2 -0
  57. package/dist/services/webWorkflowCatalog.cjs.map +1 -0
  58. package/dist/services/webWorkflowCatalog.d.cts +49 -0
  59. package/dist/services/webWorkflowCatalog.d.cts.map +1 -0
  60. package/dist/services/webWorkflowCatalog.d.mts +49 -0
  61. package/dist/services/webWorkflowCatalog.d.mts.map +1 -0
  62. package/dist/services/webWorkflowCatalog.mjs +2 -0
  63. package/dist/services/webWorkflowCatalog.mjs.map +1 -0
  64. package/dist/services/workflowLaunchCommand.cjs +2 -0
  65. package/dist/services/workflowLaunchCommand.cjs.map +1 -0
  66. package/dist/services/workflowLaunchCommand.mjs +2 -0
  67. package/dist/services/workflowLaunchCommand.mjs.map +1 -0
  68. package/dist/services/workflowRuns.cjs +3 -0
  69. package/dist/services/workflowRuns.cjs.map +1 -0
  70. package/dist/services/workflowRuns.d.cts +17 -0
  71. package/dist/services/workflowRuns.d.cts.map +1 -0
  72. package/dist/services/workflowRuns.d.mts +17 -0
  73. package/dist/services/workflowRuns.d.mts.map +1 -0
  74. package/dist/services/workflowRuns.mjs +3 -0
  75. package/dist/services/workflowRuns.mjs.map +1 -0
  76. package/dist/services/workflowTerminal.cjs +2 -0
  77. package/dist/services/workflowTerminal.cjs.map +1 -0
  78. package/dist/services/workflowTerminal.mjs +2 -0
  79. package/dist/services/workflowTerminal.mjs.map +1 -0
  80. package/dist/tui/workflow/workflowCatalog.cjs +1 -1
  81. package/dist/tui/workflow/workflowCatalog.cjs.map +1 -1
  82. package/dist/tui/workflow/workflowCatalog.mjs +1 -1
  83. package/dist/tui/workflow/workflowCatalog.mjs.map +1 -1
  84. package/dist/types/webWorkflowTerminal.cjs +2 -0
  85. package/dist/types/webWorkflowTerminal.cjs.map +1 -0
  86. package/dist/types/webWorkflowTerminal.mjs +2 -0
  87. package/dist/types/webWorkflowTerminal.mjs.map +1 -0
  88. package/dist/types/webWorkflows.cjs +2 -0
  89. package/dist/types/webWorkflows.cjs.map +1 -0
  90. package/dist/types/webWorkflows.d.cts +95 -0
  91. package/dist/types/webWorkflows.d.cts.map +1 -0
  92. package/dist/types/webWorkflows.d.mts +95 -0
  93. package/dist/types/webWorkflows.d.mts.map +1 -0
  94. package/dist/types/webWorkflows.mjs +2 -0
  95. package/dist/types/webWorkflows.mjs.map +1 -0
  96. package/dist/webHub.cjs +1 -0
  97. package/dist/webHub.d.cts +3 -0
  98. package/dist/webHub.d.mts +3 -0
  99. package/dist/webHub.mjs +1 -0
  100. package/package.json +47 -11
  101. package/src/prompts/doompi-author-workflow/SKILL.md +1 -3
  102. package/src/types/webWorkflowTerminal.ts +92 -0
  103. package/src/types/webWorkflows.ts +145 -0
  104. package/web/ArtifactsPane.tsx +433 -0
  105. package/web/LICENSE +29 -0
  106. package/web/LaunchWorkflowDialog.tsx +249 -0
  107. package/web/StepTerminalPanel.tsx +251 -0
  108. package/web/WorkflowCatalogDrawer.tsx +228 -0
  109. package/web/WorkflowToolMessage.tsx +54 -0
  110. package/web/WorkflowsActivitySection.tsx +152 -0
  111. package/web/WorkflowsPanel.tsx +737 -0
  112. package/web/ansiSpans.ts +230 -0
  113. package/web/catalogStore.ts +112 -0
  114. package/web/index.ts +61 -0
  115. package/web/launchLine.ts +66 -0
  116. package/web/runDuration.ts +15 -0
  117. package/web/terminalApi.ts +151 -0
  118. package/web/tsconfig.json +22 -0
  119. package/web/workflowActivity.ts +102 -0
  120. package/web/workflowToolRender.ts +585 -0
  121. package/web/workflowsStore.ts +90 -0
  122. package/dist/tui/workflow/terminalGrid.cjs +0 -2
  123. package/dist/tui/workflow/terminalGrid.cjs.map +0 -1
  124. package/dist/tui/workflow/terminalGrid.mjs +0 -2
  125. package/dist/tui/workflow/terminalGrid.mjs.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowLaunchCommand.cjs","names":[],"sources":["../../src/services/workflowLaunchCommand.ts"],"sourcesContent":["/**\n * The `/workflow-launch` line, parsed.\n *\n * WHY A VERB AT ALL:\n * A browser can only send a session a prompt frame, so every cockpit action\n * that has to run inside the session travels as a slash line, the way the\n * agent catalog sends `/run`. The workflow tools answer to the agent, not to a\n * page, and the leader board answers to a keyboard, so neither of them can be\n * what the launch dialog sends.\n *\n * THE SHAPE:\n * /workflow-launch <workflow> [key=value ...] [prompt words]\n *\n * Key-value pairs come first and stop at the first token that is not one, so\n * everything after them is the prompt exactly as it was typed, including any\n * `=` inside it. `runner` is the one reserved key; the rest are the workflow's\n * own `workflow_dispatch` inputs. Values may be double quoted to hold spaces.\n */\n\n/** The reserved key: which runner map entry the run executes with. */\nconst RUNNER_KEY = 'runner';\nconst KEY_VALUE = /^([A-Za-z_][\\w.-]*)=(.*)$/s;\n\nexport interface ParsedWorkflowLaunch {\n /** The workflow's name or its path, as typed; resolution belongs to the caller. */\n workflow: string;\n runner?: string;\n inputs: Record<string, string>;\n prompt?: string;\n}\n\nexport interface WorkflowLaunchParseFailure {\n error: string;\n}\n\nexport function isLaunchParseFailure(\n value: ParsedWorkflowLaunch | WorkflowLaunchParseFailure,\n): value is WorkflowLaunchParseFailure {\n return 'error' in value;\n}\n\n/** Strips one layer of surrounding double quotes, which is how a value holds spaces. */\nfunction unquote(value: string): string {\n return value.length > 1 && value.startsWith('\"') && value.endsWith('\"') ? value.slice(1, -1) : value;\n}\n\n/**\n * Splits on whitespace, keeping double-quoted runs together.\n *\n * Returns each token with the offset it started at, so the caller can take the\n * rest of the line verbatim rather than rebuilding it from tokens and losing\n * the spacing a prompt was written with.\n */\nfunction tokenize(line: string): { text: string; start: number }[] {\n const tokens: { text: string; start: number }[] = [];\n let index = 0;\n while (index < line.length) {\n while (index < line.length && /\\s/.test(line[index] as string)) index += 1;\n if (index >= line.length) break;\n const start = index;\n let quoted = false;\n while (index < line.length && (quoted || !/\\s/.test(line[index] as string))) {\n if (line[index] === '\"') quoted = !quoted;\n index += 1;\n }\n tokens.push({ text: line.slice(start, index), start });\n }\n return tokens;\n}\n\nexport function parseWorkflowLaunchCommand(args: string): ParsedWorkflowLaunch | WorkflowLaunchParseFailure {\n const tokens = tokenize(args.trim());\n const first = tokens[0];\n if (first === undefined) return { error: 'Usage: /workflow-launch <workflow> [key=value …] [prompt]' };\n\n const inputs: Record<string, string> = {};\n let runner: string | undefined;\n let index = 1;\n for (; index < tokens.length; index += 1) {\n const token = tokens[index] as { text: string; start: number };\n const match = KEY_VALUE.exec(token.text);\n if (!match) break;\n const key = match[1] as string;\n const value = unquote(match[2] as string);\n if (key === RUNNER_KEY) runner = value;\n else inputs[key] = value;\n }\n\n const rest = tokens[index];\n const prompt = rest === undefined ? undefined : args.trim().slice(rest.start).trim();\n return {\n workflow: unquote(first.text),\n ...(runner === undefined ? {} : { runner }),\n inputs,\n ...(prompt === undefined || prompt === '' ? {} : { prompt }),\n };\n}\n\n/** The line the cockpit sends, built from what its dialog collected. */\nexport function workflowLaunchCommand(request: ParsedWorkflowLaunch): string {\n const quote = (value: string): string => (/\\s/.test(value) ? `\"${value}\"` : value);\n const pairs = [\n ...(request.runner === undefined ? [] : [`${RUNNER_KEY}=${quote(request.runner)}`]),\n ...Object.entries(request.inputs).map(([key, value]) => `${key}=${quote(value)}`),\n ];\n const prompt = request.prompt?.trim();\n return ['/workflow-launch', quote(request.workflow), ...pairs, ...(prompt ? [prompt] : [])].join(' ');\n}\n\n/** One catalog row, as much of it as resolution needs. */\nexport interface LaunchResolvableWorkflow {\n name: string;\n path: string;\n relativePath: string;\n}\n\n/**\n * The workflow a token names: its name, its path in the repository, or the\n * tail of that path. Case-insensitive on the name, because the catalog shows\n * names as their author capitalised them and nobody types that back exactly.\n */\nexport function resolveWorkflowEntry<T extends LaunchResolvableWorkflow>(\n entries: readonly T[],\n token: string,\n): T | undefined {\n const needle = token.trim();\n const lower = needle.toLocaleLowerCase();\n return (\n entries.find((entry) => entry.name.toLocaleLowerCase() === lower) ??\n entries.find((entry) => entry.relativePath === needle || entry.path === needle) ??\n entries.find((entry) => entry.relativePath.endsWith(`/${needle}`) || entry.path.endsWith(`/${needle}`))\n );\n}\n\n/** What a workflow declares, as much of it as a launch has to satisfy. */\nexport interface LaunchRequirements {\n triggers: readonly string[];\n inputs: readonly { name: string; required?: boolean; options?: readonly string[] }[];\n /** Absent means the workflow names no runner map, so any runner will do. */\n runners?: readonly string[];\n}\n\n/** The trigger that makes a prompt mandatory: the run waits for one otherwise. */\nconst USER_PROMPT_TRIGGER = 'user_prompt';\n\n/**\n * Everything wrong with a launch, before it is started.\n *\n * Checked here rather than left to the engine because the failure would\n * otherwise be a run that starts and then waits forever for terminal input, or\n * one that dies on a runner its steps never declared. Both read as the launch\n * having worked.\n */\nexport function validateWorkflowLaunch(\n requirements: LaunchRequirements,\n parsed: Pick<ParsedWorkflowLaunch, 'inputs' | 'prompt' | 'runner'>,\n): string[] {\n const problems: string[] = [];\n if (requirements.triggers.includes(USER_PROMPT_TRIGGER) && (parsed.prompt ?? '') === '') {\n problems.push('This workflow is triggered by a prompt, so it needs one.');\n }\n const missing = requirements.inputs\n .filter((input) => input.required === true && (parsed.inputs[input.name] ?? '') === '')\n .map((input) => input.name);\n if (missing.length > 0)\n problems.push(`Missing required input${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}.`);\n for (const input of requirements.inputs) {\n const value = parsed.inputs[input.name];\n if (value === undefined || input.options === undefined || input.options.includes(value)) continue;\n problems.push(`Input ${input.name} must be one of: ${input.options.join(', ')}.`);\n }\n const declared = requirements.runners;\n if (parsed.runner !== undefined && declared !== undefined && !declared.includes(parsed.runner)) {\n problems.push(\n declared.length === 0\n ? 'This workflow declares no runner its steps agree on.'\n : `Runner ${parsed.runner} is not one this workflow declares: ${declared.join(', ')}.`,\n );\n }\n return problems;\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=/^([A-Za-z_][\w.-]*)=(.*)$/s;function t(e){return`error`in e}function n(e){return e.length>1&&e.startsWith(`"`)&&e.endsWith(`"`)?e.slice(1,-1):e}function r(e){let t=[],n=0;for(;n<e.length;){for(;n<e.length&&/\s/.test(e[n]);)n+=1;if(n>=e.length)break;let r=n,i=!1;for(;n<e.length&&(i||!/\s/.test(e[n]));)e[n]===`"`&&(i=!i),n+=1;t.push({text:e.slice(r,n),start:r})}return t}function i(t){let i=r(t.trim()),a=i[0];if(a===void 0)return{error:`Usage: /workflow-launch <workflow> [key=value …] [prompt]`};let o={},s,c=1;for(;c<i.length;c+=1){let t=i[c],r=e.exec(t.text);if(!r)break;let a=r[1],l=n(r[2]);a===`runner`?s=l:o[a]=l}let l=i[c],u=l===void 0?void 0:t.trim().slice(l.start).trim();return{workflow:n(a.text),...s===void 0?{}:{runner:s},inputs:o,...u===void 0||u===``?{}:{prompt:u}}}function a(e,t){let n=t.trim(),r=n.toLocaleLowerCase();return e.find(e=>e.name.toLocaleLowerCase()===r)??e.find(e=>e.relativePath===n||e.path===n)??e.find(e=>e.relativePath.endsWith(`/${n}`)||e.path.endsWith(`/${n}`))}function o(e,t){let n=[];e.triggers.includes(`user_prompt`)&&(t.prompt??``)===``&&n.push(`This workflow is triggered by a prompt, so it needs one.`);let r=e.inputs.filter(e=>e.required===!0&&(t.inputs[e.name]??``)===``).map(e=>e.name);r.length>0&&n.push(`Missing required input${r.length>1?`s`:``}: ${r.join(`, `)}.`);for(let r of e.inputs){let e=t.inputs[r.name];e===void 0||r.options===void 0||r.options.includes(e)||n.push(`Input ${r.name} must be one of: ${r.options.join(`, `)}.`)}let i=e.runners;return t.runner!==void 0&&i!==void 0&&!i.includes(t.runner)&&n.push(i.length===0?`This workflow declares no runner its steps agree on.`:`Runner ${t.runner} is not one this workflow declares: ${i.join(`, `)}.`),n}export{t as isLaunchParseFailure,i as parseWorkflowLaunchCommand,a as resolveWorkflowEntry,o as validateWorkflowLaunch};
2
+ //# sourceMappingURL=workflowLaunchCommand.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowLaunchCommand.mjs","names":[],"sources":["../../src/services/workflowLaunchCommand.ts"],"sourcesContent":["/**\n * The `/workflow-launch` line, parsed.\n *\n * WHY A VERB AT ALL:\n * A browser can only send a session a prompt frame, so every cockpit action\n * that has to run inside the session travels as a slash line, the way the\n * agent catalog sends `/run`. The workflow tools answer to the agent, not to a\n * page, and the leader board answers to a keyboard, so neither of them can be\n * what the launch dialog sends.\n *\n * THE SHAPE:\n * /workflow-launch <workflow> [key=value ...] [prompt words]\n *\n * Key-value pairs come first and stop at the first token that is not one, so\n * everything after them is the prompt exactly as it was typed, including any\n * `=` inside it. `runner` is the one reserved key; the rest are the workflow's\n * own `workflow_dispatch` inputs. Values may be double quoted to hold spaces.\n */\n\n/** The reserved key: which runner map entry the run executes with. */\nconst RUNNER_KEY = 'runner';\nconst KEY_VALUE = /^([A-Za-z_][\\w.-]*)=(.*)$/s;\n\nexport interface ParsedWorkflowLaunch {\n /** The workflow's name or its path, as typed; resolution belongs to the caller. */\n workflow: string;\n runner?: string;\n inputs: Record<string, string>;\n prompt?: string;\n}\n\nexport interface WorkflowLaunchParseFailure {\n error: string;\n}\n\nexport function isLaunchParseFailure(\n value: ParsedWorkflowLaunch | WorkflowLaunchParseFailure,\n): value is WorkflowLaunchParseFailure {\n return 'error' in value;\n}\n\n/** Strips one layer of surrounding double quotes, which is how a value holds spaces. */\nfunction unquote(value: string): string {\n return value.length > 1 && value.startsWith('\"') && value.endsWith('\"') ? value.slice(1, -1) : value;\n}\n\n/**\n * Splits on whitespace, keeping double-quoted runs together.\n *\n * Returns each token with the offset it started at, so the caller can take the\n * rest of the line verbatim rather than rebuilding it from tokens and losing\n * the spacing a prompt was written with.\n */\nfunction tokenize(line: string): { text: string; start: number }[] {\n const tokens: { text: string; start: number }[] = [];\n let index = 0;\n while (index < line.length) {\n while (index < line.length && /\\s/.test(line[index] as string)) index += 1;\n if (index >= line.length) break;\n const start = index;\n let quoted = false;\n while (index < line.length && (quoted || !/\\s/.test(line[index] as string))) {\n if (line[index] === '\"') quoted = !quoted;\n index += 1;\n }\n tokens.push({ text: line.slice(start, index), start });\n }\n return tokens;\n}\n\nexport function parseWorkflowLaunchCommand(args: string): ParsedWorkflowLaunch | WorkflowLaunchParseFailure {\n const tokens = tokenize(args.trim());\n const first = tokens[0];\n if (first === undefined) return { error: 'Usage: /workflow-launch <workflow> [key=value …] [prompt]' };\n\n const inputs: Record<string, string> = {};\n let runner: string | undefined;\n let index = 1;\n for (; index < tokens.length; index += 1) {\n const token = tokens[index] as { text: string; start: number };\n const match = KEY_VALUE.exec(token.text);\n if (!match) break;\n const key = match[1] as string;\n const value = unquote(match[2] as string);\n if (key === RUNNER_KEY) runner = value;\n else inputs[key] = value;\n }\n\n const rest = tokens[index];\n const prompt = rest === undefined ? undefined : args.trim().slice(rest.start).trim();\n return {\n workflow: unquote(first.text),\n ...(runner === undefined ? {} : { runner }),\n inputs,\n ...(prompt === undefined || prompt === '' ? {} : { prompt }),\n };\n}\n\n/** The line the cockpit sends, built from what its dialog collected. */\nexport function workflowLaunchCommand(request: ParsedWorkflowLaunch): string {\n const quote = (value: string): string => (/\\s/.test(value) ? `\"${value}\"` : value);\n const pairs = [\n ...(request.runner === undefined ? [] : [`${RUNNER_KEY}=${quote(request.runner)}`]),\n ...Object.entries(request.inputs).map(([key, value]) => `${key}=${quote(value)}`),\n ];\n const prompt = request.prompt?.trim();\n return ['/workflow-launch', quote(request.workflow), ...pairs, ...(prompt ? [prompt] : [])].join(' ');\n}\n\n/** One catalog row, as much of it as resolution needs. */\nexport interface LaunchResolvableWorkflow {\n name: string;\n path: string;\n relativePath: string;\n}\n\n/**\n * The workflow a token names: its name, its path in the repository, or the\n * tail of that path. Case-insensitive on the name, because the catalog shows\n * names as their author capitalised them and nobody types that back exactly.\n */\nexport function resolveWorkflowEntry<T extends LaunchResolvableWorkflow>(\n entries: readonly T[],\n token: string,\n): T | undefined {\n const needle = token.trim();\n const lower = needle.toLocaleLowerCase();\n return (\n entries.find((entry) => entry.name.toLocaleLowerCase() === lower) ??\n entries.find((entry) => entry.relativePath === needle || entry.path === needle) ??\n entries.find((entry) => entry.relativePath.endsWith(`/${needle}`) || entry.path.endsWith(`/${needle}`))\n );\n}\n\n/** What a workflow declares, as much of it as a launch has to satisfy. */\nexport interface LaunchRequirements {\n triggers: readonly string[];\n inputs: readonly { name: string; required?: boolean; options?: readonly string[] }[];\n /** Absent means the workflow names no runner map, so any runner will do. */\n runners?: readonly string[];\n}\n\n/** The trigger that makes a prompt mandatory: the run waits for one otherwise. */\nconst USER_PROMPT_TRIGGER = 'user_prompt';\n\n/**\n * Everything wrong with a launch, before it is started.\n *\n * Checked here rather than left to the engine because the failure would\n * otherwise be a run that starts and then waits forever for terminal input, or\n * one that dies on a runner its steps never declared. Both read as the launch\n * having worked.\n */\nexport function validateWorkflowLaunch(\n requirements: LaunchRequirements,\n parsed: Pick<ParsedWorkflowLaunch, 'inputs' | 'prompt' | 'runner'>,\n): string[] {\n const problems: string[] = [];\n if (requirements.triggers.includes(USER_PROMPT_TRIGGER) && (parsed.prompt ?? '') === '') {\n problems.push('This workflow is triggered by a prompt, so it needs one.');\n }\n const missing = requirements.inputs\n .filter((input) => input.required === true && (parsed.inputs[input.name] ?? '') === '')\n .map((input) => input.name);\n if (missing.length > 0)\n problems.push(`Missing required input${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}.`);\n for (const input of requirements.inputs) {\n const value = parsed.inputs[input.name];\n if (value === undefined || input.options === undefined || input.options.includes(value)) continue;\n problems.push(`Input ${input.name} must be one of: ${input.options.join(', ')}.`);\n }\n const declared = requirements.runners;\n if (parsed.runner !== undefined && declared !== undefined && !declared.includes(parsed.runner)) {\n problems.push(\n declared.length === 0\n ? 'This workflow declares no runner its steps agree on.'\n : `Runner ${parsed.runner} is not one this workflow declares: ${declared.join(', ')}.`,\n );\n }\n return problems;\n}\n"],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ let e=require("node:path");const t=[`running`,`completed`,`error`],n=864e5,r=`PI_SESSION_ID`,i=new Set([`running`,`completed`,`skipped`,`failed`,`pause_requested`,`paused`,`resumed`]),a=new Set([`success`,`skipped`,`failed`,`interrupted`]),o=new Set([`running`,`pause_requested`,`paused`,`resume_requested`]),s=new Set([`completed`,`skipped`,`failed`]),c=`post`,l=/\u001b\[[0-9;]*[A-Za-z]/g;function u(t){return(0,e.resolve)(t.envValue!==void 0&&t.envValue!==``?t.envValue:(0,e.resolve)(t.homeDir,`.workflow-mcp`))}function d(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function f(e){return typeof e==`string`&&e!==``?e:void 0}function p(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function m(e){return e.replace(l,``).trim()}function h(e){let n;try{n=JSON.parse(e)}catch{return}if(!d(n))return;let i=f(n.runKey),s=f(n.workspace),c=f(n.workflowPath),l=f(n.startedAt),u=f(n.stage);if(!i||!s||!c||!l||u===void 0||!t.includes(u))return;let p=f(n.outcome),h=f(n.executionState),g=f(n.errorMessage),_=d(n.env)?n.env:void 0,v={runKey:i,workspace:s,displayName:f(n.displayName)??i,...f(n.workflowName)===void 0?{}:{workflowName:f(n.workflowName)},workflowPath:c,stage:u,...p!==void 0&&a.has(p)?{outcome:p}:{},...h!==void 0&&o.has(h)?{executionState:h}:{},...f(n.prompt)===void 0?{}:{prompt:f(n.prompt)},startedAt:l,...f(n.finishedAt)===void 0?{}:{finishedAt:f(n.finishedAt)},...g===void 0?{}:{errorMessage:m(g)},...f(n.failedJob)===void 0?{}:{failedJob:f(n.failedJob)},...n.stale===!0?{stale:!0}:{},...f(n.staleReason)===void 0?{}:{staleReason:f(n.staleReason)},...f(n.worktreeBranch)===void 0?{}:{worktreeBranch:f(n.worktreeBranch)},jobs:[]},y=_===void 0?void 0:f(_[r]),b=f(n.originalRepoPath);return{view:v,...y===void 0?{}:{piSessionId:y},...b===void 0?{}:{originalRepoPath:b}}}function g(e){let t=[];for(let n of e.split(`
2
+ `)){let e=n.trim();if(e===``)continue;let r;try{r=JSON.parse(e)}catch{continue}if(!d(r))continue;let a=r.type,o=f(r.status),s=f(r.job),c=f(r.at);if(a!==`job`&&a!==`step`||!s||!c||o===void 0||!i.has(o))continue;let l=f(r.step),u=p(r.index),m=p(r.total),h=f(r.reason);t.push({type:a,status:o,job:s,...l===void 0?{}:{step:l},...u===void 0?{}:{index:u},...m===void 0?{}:{total:m},...h===void 0?{}:{reason:h},at:c})}return t}function _(e){return e===`pre`?`pre`:e===c?c:`job`}function v(e){let t=[],n=new Map;for(let r of e){let e=n.get(r.job);if(e===void 0&&(e={name:r.job,phase:_(r.job),status:r.status,steps:[]},n.set(r.job,e),t.push(e)),r.type===`job`){e.status=r.status,r.reason!==void 0&&(e.reason=r.reason),r.index!==void 0&&(e.index=r.index),r.total!==void 0&&(e.total=r.total),r.status===`running`&&e.startedAt===void 0&&(e.startedAt=r.at),s.has(r.status)&&(e.endedAt=r.at);continue}if(r.step===void 0)continue;let i=e.steps.find(e=>e.name===r.step);i===void 0&&(i={name:r.step,status:r.status},e.steps.push(i)),i.status=r.status,r.reason!==void 0&&(i.reason=r.reason),r.status===`running`&&i.startedAt===void 0&&(i.startedAt=r.at),s.has(r.status)&&(i.endedAt=r.at)}return t}const y=new Set([`running`,`pause_requested`,`paused`,`resumed`]);function b(e){for(let t=e.length-1;t>=0;--t){let n=e[t];if(y.has(n.status)){for(let e=n.steps.length-1;e>=0;--e){let t=n.steps[e];if(y.has(t.status))return{job:n.name,step:t.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}return{job:n.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}}}function x(e,t){let n=e.stage===`running`?b(t):void 0;return{...e,jobs:t,...n===void 0?{}:{position:n}}}function S(e,t){return e===t||e.startsWith(`${t}/`)||t.startsWith(`${e}/`)}function C(e,t){return e.piSessionId!==void 0&&e.piSessionId===t.sessionId||e.originalRepoPath!==void 0&&S(e.originalRepoPath,t.cwd)?!0:S(e.view.workflowPath,t.cwd)}function w(e){if(e===void 0)return 0;let t=Date.parse(e);return Number.isFinite(t)?t:0}const T={running:0,error:1,completed:2};function E(e,t){return e.stage!==`error`||t-(w(e.finishedAt)||w(e.startedAt))<n}function D(e){return e.stage===`running`?w(e.startedAt):w(e.finishedAt)||w(e.startedAt)}function O(e,t){let n=new Map;for(let r of e){if(!E(r,t))continue;let e=n.get(r.stage);e?e.push(r):n.set(r.stage,[r])}return[...n.entries()].sort(([e],[t])=>T[e]-T[t]).flatMap(([,e])=>e.sort((e,t)=>D(t)-D(e)).slice(0,24))}exports.PI_SESSION_ENV=r,exports.PROGRESS_FILE_NAME=`progress.ndjson`,exports.RUN_RECORD_FILE_NAME=`run.json`,exports.WORKFLOW_ERROR_RETENTION_MS=n,exports.WORKFLOW_HOME_ENV=`WORKFLOW_MCP_HOME`,exports.WORKFLOW_STAGES=t,exports.WORKSPACES_DIR_NAME=`workspaces`,exports.completeWorkflowRunView=x,exports.foldWorkflowProgress=v,exports.parseWorkflowProgress=g,exports.parseWorkflowRunRecord=h,exports.presentWorkflowRuns=O,exports.resolveWorkflowHome=u,exports.runBelongsToSession=C,exports.workflowPosition=b;
3
+ //# sourceMappingURL=workflowRuns.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowRuns.cjs","names":["resolve"],"sources":["../../src/services/workflowRuns.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport type {\n WorkflowJobPhase,\n WorkflowJobView,\n WorkflowOutcome,\n WorkflowPosition,\n WorkflowProgressState,\n WorkflowRunView,\n WorkflowStage,\n WorkflowStepView,\n} from '../types/webWorkflows.ts';\n\n/**\n * MIRRORS @agimon-ai/workflow-mcp's on-disk registry\n * (src/services/WorkflowRegistryService.ts and WorkflowProgressService.ts in\n * that package). The engine keeps one directory per run under\n * `<home>/workspaces/<workspace>/<stage>/<runKey>/` holding a `run.json`\n * record and an append-only `progress.ndjson` job/step log; the hub only ever\n * reads them. The derivation is duplicated rather than imported because\n * doompi-web must not depend on the engine at runtime; the unit suite pins\n * this mirror against the package's published types.\n */\nconst DEFAULT_WORKFLOW_HOME_DIR_NAME = '.workflow-mcp';\nexport const WORKFLOW_HOME_ENV = 'WORKFLOW_MCP_HOME';\nexport const WORKSPACES_DIR_NAME = 'workspaces';\nexport const RUN_RECORD_FILE_NAME = 'run.json';\nexport const PROGRESS_FILE_NAME = 'progress.ndjson';\nexport const WORKFLOW_STAGES: readonly WorkflowStage[] = ['running', 'completed', 'error'];\n\n/**\n * Errored runs stay a day, which is how long recovery has to act on them.\n *\n * Finished runs have no retention of their own: a session keeps every run it\n * started, grouped by outcome, for as long as the session lives. A run leaves\n * the view when the engine prunes its registry directory, not on a timer here,\n * because \"what did this session run today\" is a question asked long after the\n * run settled.\n */\nexport const WORKFLOW_ERROR_RETENTION_MS = 24 * 60 * 60 * 1000;\n/**\n * Upper bound per outcome group rather than over the whole list.\n *\n * One cap across every run let a long stream of green runs push a failure out\n * of the list, which is the one thing that must never fall off the end.\n */\nexport const MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP = 24;\n\n/** The env key doompi-workflow stamps on runs it launches; ties a run to a Pi session. */\nexport const PI_SESSION_ENV = 'PI_SESSION_ID';\n\nconst PROGRESS_STATES: ReadonlySet<string> = new Set([\n 'running',\n 'completed',\n 'skipped',\n 'failed',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\nconst OUTCOMES: ReadonlySet<string> = new Set(['success', 'skipped', 'failed', 'interrupted']);\nconst EXECUTION_STATES: ReadonlySet<string> = new Set(['running', 'pause_requested', 'paused', 'resume_requested']);\nconst STEP_TERMINAL_STATES: ReadonlySet<WorkflowProgressState> = new Set(['completed', 'skipped', 'failed']);\n/** The engine records its pre:/post: blocks under these reserved pseudo-job names. */\nconst PHASE_JOB_PRE = 'pre';\nconst PHASE_JOB_POST = 'post';\n\n// oxlint-disable-next-line no-control-regex -- stripping terminal color codes from errorMessage is the point\nconst ANSI_ESCAPE_PATTERN = /\\u001b\\[[0-9;]*[A-Za-z]/g;\n\nexport interface WorkflowHomeInput {\n /** process.env.WORKFLOW_MCP_HOME, supplied by the caller. */\n envValue: string | undefined;\n /** os.homedir(), supplied by the caller. */\n homeDir: string;\n}\n\n/** Where workflow-mcp keeps its registry, mirroring the engine's own resolution. */\nexport function resolveWorkflowHome(input: WorkflowHomeInput): string {\n return resolve(\n input.envValue !== undefined && input.envValue !== ''\n ? input.envValue\n : resolve(input.homeDir, DEFAULT_WORKFLOW_HOME_DIR_NAME),\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction asOptionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n\nfunction asOptionalNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction stripAnsi(text: string): string {\n return text.replace(ANSI_ESCAPE_PATTERN, '').trim();\n}\n\n/**\n * One run record plus the facts the hub needs for session scoping but the\n * page never sees.\n */\nexport interface ParsedWorkflowRun {\n /** The wire view, before the progress log is folded in (jobs empty). */\n view: WorkflowRunView;\n /** env.PI_SESSION_ID from the record, when the launcher stamped one. */\n piSessionId?: string;\n /** The repository the run was launched from, when recorded. */\n originalRepoPath?: string;\n}\n\n/**\n * Validates one run.json into the wire shape plus scoping facts.\n *\n * Returns undefined for anything unreadable or a foreign format. The record\n * is trusted for its own stage: the registry rewrites it when a run moves\n * between stage directories.\n */\nexport function parseWorkflowRunRecord(raw: string): ParsedWorkflowRun | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (!isRecord(parsed)) return undefined;\n const runKey = asOptionalString(parsed.runKey);\n const workspace = asOptionalString(parsed.workspace);\n const workflowPath = asOptionalString(parsed.workflowPath);\n const startedAt = asOptionalString(parsed.startedAt);\n const stage = asOptionalString(parsed.stage);\n if (!runKey || !workspace || !workflowPath || !startedAt) return undefined;\n if (stage === undefined || !WORKFLOW_STAGES.includes(stage as WorkflowStage)) return undefined;\n\n const outcome = asOptionalString(parsed.outcome);\n const executionState = asOptionalString(parsed.executionState);\n const rawError = asOptionalString(parsed.errorMessage);\n const env = isRecord(parsed.env) ? parsed.env : undefined;\n const view: WorkflowRunView = {\n runKey,\n workspace,\n displayName: asOptionalString(parsed.displayName) ?? runKey,\n ...(asOptionalString(parsed.workflowName) === undefined\n ? {}\n : { workflowName: asOptionalString(parsed.workflowName) }),\n workflowPath,\n stage: stage as WorkflowStage,\n ...(outcome !== undefined && OUTCOMES.has(outcome) ? { outcome: outcome as WorkflowOutcome } : {}),\n ...(executionState !== undefined && EXECUTION_STATES.has(executionState)\n ? { executionState: executionState as WorkflowRunView['executionState'] }\n : {}),\n ...(asOptionalString(parsed.prompt) === undefined ? {} : { prompt: asOptionalString(parsed.prompt) }),\n startedAt,\n ...(asOptionalString(parsed.finishedAt) === undefined ? {} : { finishedAt: asOptionalString(parsed.finishedAt) }),\n ...(rawError === undefined ? {} : { errorMessage: stripAnsi(rawError) }),\n ...(asOptionalString(parsed.failedJob) === undefined ? {} : { failedJob: asOptionalString(parsed.failedJob) }),\n ...(parsed.stale === true ? { stale: true } : {}),\n ...(asOptionalString(parsed.staleReason) === undefined\n ? {}\n : { staleReason: asOptionalString(parsed.staleReason) }),\n ...(asOptionalString(parsed.worktreeBranch) === undefined\n ? {}\n : { worktreeBranch: asOptionalString(parsed.worktreeBranch) }),\n jobs: [],\n };\n const piSessionId = env === undefined ? undefined : asOptionalString(env[PI_SESSION_ENV]);\n const originalRepoPath = asOptionalString(parsed.originalRepoPath);\n return {\n view,\n ...(piSessionId === undefined ? {} : { piSessionId }),\n ...(originalRepoPath === undefined ? {} : { originalRepoPath }),\n };\n}\n\ninterface WorkflowProgressEvent {\n type: 'job' | 'step';\n status: WorkflowProgressState;\n job: string;\n step?: string;\n index?: number;\n total?: number;\n reason?: string;\n at: string;\n}\n\n/**\n * Parses the append-only progress log. Malformed lines are skipped rather\n * than failing the file: a reader routinely catches a torn final line\n * mid-append.\n */\nexport function parseWorkflowProgress(raw: string): WorkflowProgressEvent[] {\n const events: WorkflowProgressEvent[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '') continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (!isRecord(parsed)) continue;\n const type = parsed.type;\n const status = asOptionalString(parsed.status);\n const job = asOptionalString(parsed.job);\n const at = asOptionalString(parsed.at);\n if ((type !== 'job' && type !== 'step') || !job || !at) continue;\n if (status === undefined || !PROGRESS_STATES.has(status)) continue;\n const step = asOptionalString(parsed.step);\n const index = asOptionalNumber(parsed.index);\n const total = asOptionalNumber(parsed.total);\n const reason = asOptionalString(parsed.reason);\n events.push({\n type,\n status: status as WorkflowProgressState,\n job,\n ...(step === undefined ? {} : { step }),\n ...(index === undefined ? {} : { index }),\n ...(total === undefined ? {} : { total }),\n ...(reason === undefined ? {} : { reason }),\n at,\n });\n }\n return events;\n}\n\nfunction jobPhase(name: string): WorkflowJobPhase {\n if (name === PHASE_JOB_PRE) return PHASE_JOB_PRE;\n if (name === PHASE_JOB_POST) return PHASE_JOB_POST;\n return 'job';\n}\n\n/**\n * Folds the event log into the current job tree; later events win, so a job\n * that started and later failed reads as failed. Jobs and steps keep the\n * order of their first appearance.\n */\nexport function foldWorkflowProgress(events: readonly WorkflowProgressEvent[]): WorkflowJobView[] {\n const jobs: WorkflowJobView[] = [];\n const jobByName = new Map<string, WorkflowJobView>();\n for (const event of events) {\n let job = jobByName.get(event.job);\n if (job === undefined) {\n job = { name: event.job, phase: jobPhase(event.job), status: event.status, steps: [] };\n jobByName.set(event.job, job);\n jobs.push(job);\n }\n if (event.type === 'job') {\n job.status = event.status;\n if (event.reason !== undefined) job.reason = event.reason;\n if (event.index !== undefined) job.index = event.index;\n if (event.total !== undefined) job.total = event.total;\n if (event.status === 'running' && job.startedAt === undefined) job.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) job.endedAt = event.at;\n continue;\n }\n if (event.step === undefined) continue;\n let step: WorkflowStepView | undefined = job.steps.find((candidate) => candidate.name === event.step);\n if (step === undefined) {\n step = { name: event.step, status: event.status };\n job.steps.push(step);\n }\n step.status = event.status;\n if (event.reason !== undefined) step.reason = event.reason;\n if (event.status === 'running' && step.startedAt === undefined) step.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) step.endedAt = event.at;\n }\n return jobs;\n}\n\nconst ACTIVE_PROGRESS_STATES: ReadonlySet<WorkflowProgressState> = new Set([\n 'running',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\n\n/** The job and step a run is on right now, for the NOW breadcrumb. */\nexport function workflowPosition(jobs: readonly WorkflowJobView[]): WorkflowPosition | undefined {\n for (let jobIndex = jobs.length - 1; jobIndex >= 0; jobIndex -= 1) {\n const job = jobs[jobIndex]!;\n if (!ACTIVE_PROGRESS_STATES.has(job.status)) continue;\n for (let stepIndex = job.steps.length - 1; stepIndex >= 0; stepIndex -= 1) {\n const step = job.steps[stepIndex]!;\n if (ACTIVE_PROGRESS_STATES.has(step.status)) {\n return {\n job: job.name,\n step: step.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n }\n return {\n job: job.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n return undefined;\n}\n\n/** The finished view: record fields plus the folded job tree and position. */\nexport function completeWorkflowRunView(view: WorkflowRunView, jobs: WorkflowJobView[]): WorkflowRunView {\n const position = view.stage === 'running' ? workflowPosition(jobs) : undefined;\n return { ...view, jobs, ...(position === undefined ? {} : { position }) };\n}\n\nexport interface WorkflowScopeInput {\n sessionId: string;\n /** The session's working directory; repo-local runs belong to it too. */\n cwd: string;\n}\n\nfunction pathsOverlap(left: string, right: string): boolean {\n return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);\n}\n\n/**\n * Whether a run belongs on a session's workflow tab: launched by that Pi\n * session (the same env test doompi-workflow's isSessionRun applies), or\n * launched from the repository the session works in. Containment goes both\n * ways so a session rooted in a subdirectory still sees its repo's runs.\n */\nexport function runBelongsToSession(run: ParsedWorkflowRun, scope: WorkflowScopeInput): boolean {\n if (run.piSessionId !== undefined && run.piSessionId === scope.sessionId) return true;\n if (run.originalRepoPath !== undefined && pathsOverlap(run.originalRepoPath, scope.cwd)) return true;\n return pathsOverlap(run.view.workflowPath, scope.cwd);\n}\n\nfunction parseTime(value: string | undefined): number {\n if (value === undefined) return 0;\n const time = Date.parse(value);\n return Number.isFinite(time) ? time : 0;\n}\n\nconst STAGE_ORDER: Readonly<Record<WorkflowStage, number>> = { running: 0, error: 1, completed: 2 };\n\n/**\n * Whether a run still belongs in the view.\n *\n * Only errored runs age out here, and slowly: everything else a session\n * started stays until the engine forgets the run itself.\n */\nfunction withinRetention(run: WorkflowRunView, now: number): boolean {\n if (run.stage !== 'error') return true;\n const settledAt = parseTime(run.finishedAt) || parseTime(run.startedAt);\n return now - settledAt < WORKFLOW_ERROR_RETENTION_MS;\n}\n\n/**\n * When a run last moved, which is what orders it within its group.\n *\n * A running run has only its start; a settled one is ordered by when it\n * settled, because a list kept for the whole session is read as a history and\n * two runs started together can finish an hour apart.\n */\nfunction lastMovedAt(run: WorkflowRunView): number {\n return run.stage === 'running' ? parseTime(run.startedAt) : parseTime(run.finishedAt) || parseTime(run.startedAt);\n}\n\n/**\n * Running runs first, then errored (recovery acts on them), then finished;\n * newest first within a group, and each group capped on its own.\n */\nexport function presentWorkflowRuns(runs: readonly WorkflowRunView[], now: number): WorkflowRunView[] {\n const groups = new Map<WorkflowStage, WorkflowRunView[]>();\n for (const run of runs) {\n if (!withinRetention(run, now)) continue;\n const group = groups.get(run.stage);\n if (group) group.push(run);\n else groups.set(run.stage, [run]);\n }\n return [...groups.entries()]\n .sort(([left], [right]) => STAGE_ORDER[left] - STAGE_ORDER[right])\n .flatMap(([, group]) =>\n group\n .sort((left, right) => lastMovedAt(right) - lastMovedAt(left))\n .slice(0, MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP),\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import { WorkflowRunView } from "../types/webWorkflows.cjs";
2
+ //#region src/services/workflowRuns.d.ts
3
+ /**
4
+ * One run record plus the facts the hub needs for session scoping but the
5
+ * page never sees.
6
+ */
7
+ interface ParsedWorkflowRun {
8
+ /** The wire view, before the progress log is folded in (jobs empty). */
9
+ view: WorkflowRunView;
10
+ /** env.PI_SESSION_ID from the record, when the launcher stamped one. */
11
+ piSessionId?: string;
12
+ /** The repository the run was launched from, when recorded. */
13
+ originalRepoPath?: string;
14
+ }
15
+ //#endregion
16
+ export { ParsedWorkflowRun };
17
+ //# sourceMappingURL=workflowRuns.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowRuns.d.cts","names":[],"sources":["../../src/services/workflowRuns.ts"],"mappings":";;;;;;UAyGiB;;EAEf,MAAM;;EAEN;;EAEA"}
@@ -0,0 +1,17 @@
1
+ import { WorkflowRunView } from "../types/webWorkflows.mjs";
2
+ //#region src/services/workflowRuns.d.ts
3
+ /**
4
+ * One run record plus the facts the hub needs for session scoping but the
5
+ * page never sees.
6
+ */
7
+ interface ParsedWorkflowRun {
8
+ /** The wire view, before the progress log is folded in (jobs empty). */
9
+ view: WorkflowRunView;
10
+ /** env.PI_SESSION_ID from the record, when the launcher stamped one. */
11
+ piSessionId?: string;
12
+ /** The repository the run was launched from, when recorded. */
13
+ originalRepoPath?: string;
14
+ }
15
+ //#endregion
16
+ export { ParsedWorkflowRun };
17
+ //# sourceMappingURL=workflowRuns.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowRuns.d.mts","names":[],"sources":["../../src/services/workflowRuns.ts"],"mappings":";;;;;;UAyGiB;;EAEf,MAAM;;EAEN;;EAEA"}
@@ -0,0 +1,3 @@
1
+ import{resolve as e}from"node:path";const t=`WORKFLOW_MCP_HOME`,n=`workspaces`,r=`run.json`,i=`progress.ndjson`,a=[`running`,`completed`,`error`],o=864e5,s=`PI_SESSION_ID`,c=new Set([`running`,`completed`,`skipped`,`failed`,`pause_requested`,`paused`,`resumed`]),l=new Set([`success`,`skipped`,`failed`,`interrupted`]),u=new Set([`running`,`pause_requested`,`paused`,`resume_requested`]),d=new Set([`completed`,`skipped`,`failed`]),f=`post`,p=/\u001b\[[0-9;]*[A-Za-z]/g;function m(t){return e(t.envValue!==void 0&&t.envValue!==``?t.envValue:e(t.homeDir,`.workflow-mcp`))}function h(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function g(e){return typeof e==`string`&&e!==``?e:void 0}function _(e){return typeof e==`number`&&Number.isFinite(e)?e:void 0}function v(e){return e.replace(p,``).trim()}function y(e){let t;try{t=JSON.parse(e)}catch{return}if(!h(t))return;let n=g(t.runKey),r=g(t.workspace),i=g(t.workflowPath),o=g(t.startedAt),c=g(t.stage);if(!n||!r||!i||!o||c===void 0||!a.includes(c))return;let d=g(t.outcome),f=g(t.executionState),p=g(t.errorMessage),m=h(t.env)?t.env:void 0,_={runKey:n,workspace:r,displayName:g(t.displayName)??n,...g(t.workflowName)===void 0?{}:{workflowName:g(t.workflowName)},workflowPath:i,stage:c,...d!==void 0&&l.has(d)?{outcome:d}:{},...f!==void 0&&u.has(f)?{executionState:f}:{},...g(t.prompt)===void 0?{}:{prompt:g(t.prompt)},startedAt:o,...g(t.finishedAt)===void 0?{}:{finishedAt:g(t.finishedAt)},...p===void 0?{}:{errorMessage:v(p)},...g(t.failedJob)===void 0?{}:{failedJob:g(t.failedJob)},...t.stale===!0?{stale:!0}:{},...g(t.staleReason)===void 0?{}:{staleReason:g(t.staleReason)},...g(t.worktreeBranch)===void 0?{}:{worktreeBranch:g(t.worktreeBranch)},jobs:[]},y=m===void 0?void 0:g(m[s]),b=g(t.originalRepoPath);return{view:_,...y===void 0?{}:{piSessionId:y},...b===void 0?{}:{originalRepoPath:b}}}function b(e){let t=[];for(let n of e.split(`
2
+ `)){let e=n.trim();if(e===``)continue;let r;try{r=JSON.parse(e)}catch{continue}if(!h(r))continue;let i=r.type,a=g(r.status),o=g(r.job),s=g(r.at);if(i!==`job`&&i!==`step`||!o||!s||a===void 0||!c.has(a))continue;let l=g(r.step),u=_(r.index),d=_(r.total),f=g(r.reason);t.push({type:i,status:a,job:o,...l===void 0?{}:{step:l},...u===void 0?{}:{index:u},...d===void 0?{}:{total:d},...f===void 0?{}:{reason:f},at:s})}return t}function x(e){return e===`pre`?`pre`:e===f?f:`job`}function S(e){let t=[],n=new Map;for(let r of e){let e=n.get(r.job);if(e===void 0&&(e={name:r.job,phase:x(r.job),status:r.status,steps:[]},n.set(r.job,e),t.push(e)),r.type===`job`){e.status=r.status,r.reason!==void 0&&(e.reason=r.reason),r.index!==void 0&&(e.index=r.index),r.total!==void 0&&(e.total=r.total),r.status===`running`&&e.startedAt===void 0&&(e.startedAt=r.at),d.has(r.status)&&(e.endedAt=r.at);continue}if(r.step===void 0)continue;let i=e.steps.find(e=>e.name===r.step);i===void 0&&(i={name:r.step,status:r.status},e.steps.push(i)),i.status=r.status,r.reason!==void 0&&(i.reason=r.reason),r.status===`running`&&i.startedAt===void 0&&(i.startedAt=r.at),d.has(r.status)&&(i.endedAt=r.at)}return t}const C=new Set([`running`,`pause_requested`,`paused`,`resumed`]);function w(e){for(let t=e.length-1;t>=0;--t){let n=e[t];if(C.has(n.status)){for(let e=n.steps.length-1;e>=0;--e){let t=n.steps[e];if(C.has(t.status))return{job:n.name,step:t.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}return{job:n.name,...n.index===void 0?{}:{index:n.index},...n.total===void 0?{}:{total:n.total}}}}}function T(e,t){let n=e.stage===`running`?w(t):void 0;return{...e,jobs:t,...n===void 0?{}:{position:n}}}function E(e,t){return e===t||e.startsWith(`${t}/`)||t.startsWith(`${e}/`)}function D(e,t){return e.piSessionId!==void 0&&e.piSessionId===t.sessionId||e.originalRepoPath!==void 0&&E(e.originalRepoPath,t.cwd)?!0:E(e.view.workflowPath,t.cwd)}function O(e){if(e===void 0)return 0;let t=Date.parse(e);return Number.isFinite(t)?t:0}const k={running:0,error:1,completed:2};function A(e,t){return e.stage!==`error`||t-(O(e.finishedAt)||O(e.startedAt))<o}function j(e){return e.stage===`running`?O(e.startedAt):O(e.finishedAt)||O(e.startedAt)}function M(e,t){let n=new Map;for(let r of e){if(!A(r,t))continue;let e=n.get(r.stage);e?e.push(r):n.set(r.stage,[r])}return[...n.entries()].sort(([e],[t])=>k[e]-k[t]).flatMap(([,e])=>e.sort((e,t)=>j(t)-j(e)).slice(0,24))}export{s as PI_SESSION_ENV,i as PROGRESS_FILE_NAME,r as RUN_RECORD_FILE_NAME,o as WORKFLOW_ERROR_RETENTION_MS,t as WORKFLOW_HOME_ENV,a as WORKFLOW_STAGES,n as WORKSPACES_DIR_NAME,T as completeWorkflowRunView,S as foldWorkflowProgress,b as parseWorkflowProgress,y as parseWorkflowRunRecord,M as presentWorkflowRuns,m as resolveWorkflowHome,D as runBelongsToSession,w as workflowPosition};
3
+ //# sourceMappingURL=workflowRuns.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowRuns.mjs","names":[],"sources":["../../src/services/workflowRuns.ts"],"sourcesContent":["import { resolve } from 'node:path';\nimport type {\n WorkflowJobPhase,\n WorkflowJobView,\n WorkflowOutcome,\n WorkflowPosition,\n WorkflowProgressState,\n WorkflowRunView,\n WorkflowStage,\n WorkflowStepView,\n} from '../types/webWorkflows.ts';\n\n/**\n * MIRRORS @agimon-ai/workflow-mcp's on-disk registry\n * (src/services/WorkflowRegistryService.ts and WorkflowProgressService.ts in\n * that package). The engine keeps one directory per run under\n * `<home>/workspaces/<workspace>/<stage>/<runKey>/` holding a `run.json`\n * record and an append-only `progress.ndjson` job/step log; the hub only ever\n * reads them. The derivation is duplicated rather than imported because\n * doompi-web must not depend on the engine at runtime; the unit suite pins\n * this mirror against the package's published types.\n */\nconst DEFAULT_WORKFLOW_HOME_DIR_NAME = '.workflow-mcp';\nexport const WORKFLOW_HOME_ENV = 'WORKFLOW_MCP_HOME';\nexport const WORKSPACES_DIR_NAME = 'workspaces';\nexport const RUN_RECORD_FILE_NAME = 'run.json';\nexport const PROGRESS_FILE_NAME = 'progress.ndjson';\nexport const WORKFLOW_STAGES: readonly WorkflowStage[] = ['running', 'completed', 'error'];\n\n/**\n * Errored runs stay a day, which is how long recovery has to act on them.\n *\n * Finished runs have no retention of their own: a session keeps every run it\n * started, grouped by outcome, for as long as the session lives. A run leaves\n * the view when the engine prunes its registry directory, not on a timer here,\n * because \"what did this session run today\" is a question asked long after the\n * run settled.\n */\nexport const WORKFLOW_ERROR_RETENTION_MS = 24 * 60 * 60 * 1000;\n/**\n * Upper bound per outcome group rather than over the whole list.\n *\n * One cap across every run let a long stream of green runs push a failure out\n * of the list, which is the one thing that must never fall off the end.\n */\nexport const MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP = 24;\n\n/** The env key doompi-workflow stamps on runs it launches; ties a run to a Pi session. */\nexport const PI_SESSION_ENV = 'PI_SESSION_ID';\n\nconst PROGRESS_STATES: ReadonlySet<string> = new Set([\n 'running',\n 'completed',\n 'skipped',\n 'failed',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\nconst OUTCOMES: ReadonlySet<string> = new Set(['success', 'skipped', 'failed', 'interrupted']);\nconst EXECUTION_STATES: ReadonlySet<string> = new Set(['running', 'pause_requested', 'paused', 'resume_requested']);\nconst STEP_TERMINAL_STATES: ReadonlySet<WorkflowProgressState> = new Set(['completed', 'skipped', 'failed']);\n/** The engine records its pre:/post: blocks under these reserved pseudo-job names. */\nconst PHASE_JOB_PRE = 'pre';\nconst PHASE_JOB_POST = 'post';\n\n// oxlint-disable-next-line no-control-regex -- stripping terminal color codes from errorMessage is the point\nconst ANSI_ESCAPE_PATTERN = /\\u001b\\[[0-9;]*[A-Za-z]/g;\n\nexport interface WorkflowHomeInput {\n /** process.env.WORKFLOW_MCP_HOME, supplied by the caller. */\n envValue: string | undefined;\n /** os.homedir(), supplied by the caller. */\n homeDir: string;\n}\n\n/** Where workflow-mcp keeps its registry, mirroring the engine's own resolution. */\nexport function resolveWorkflowHome(input: WorkflowHomeInput): string {\n return resolve(\n input.envValue !== undefined && input.envValue !== ''\n ? input.envValue\n : resolve(input.homeDir, DEFAULT_WORKFLOW_HOME_DIR_NAME),\n );\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nfunction asOptionalString(value: unknown): string | undefined {\n return typeof value === 'string' && value !== '' ? value : undefined;\n}\n\nfunction asOptionalNumber(value: unknown): number | undefined {\n return typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n}\n\nfunction stripAnsi(text: string): string {\n return text.replace(ANSI_ESCAPE_PATTERN, '').trim();\n}\n\n/**\n * One run record plus the facts the hub needs for session scoping but the\n * page never sees.\n */\nexport interface ParsedWorkflowRun {\n /** The wire view, before the progress log is folded in (jobs empty). */\n view: WorkflowRunView;\n /** env.PI_SESSION_ID from the record, when the launcher stamped one. */\n piSessionId?: string;\n /** The repository the run was launched from, when recorded. */\n originalRepoPath?: string;\n}\n\n/**\n * Validates one run.json into the wire shape plus scoping facts.\n *\n * Returns undefined for anything unreadable or a foreign format. The record\n * is trusted for its own stage: the registry rewrites it when a run moves\n * between stage directories.\n */\nexport function parseWorkflowRunRecord(raw: string): ParsedWorkflowRun | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return undefined;\n }\n if (!isRecord(parsed)) return undefined;\n const runKey = asOptionalString(parsed.runKey);\n const workspace = asOptionalString(parsed.workspace);\n const workflowPath = asOptionalString(parsed.workflowPath);\n const startedAt = asOptionalString(parsed.startedAt);\n const stage = asOptionalString(parsed.stage);\n if (!runKey || !workspace || !workflowPath || !startedAt) return undefined;\n if (stage === undefined || !WORKFLOW_STAGES.includes(stage as WorkflowStage)) return undefined;\n\n const outcome = asOptionalString(parsed.outcome);\n const executionState = asOptionalString(parsed.executionState);\n const rawError = asOptionalString(parsed.errorMessage);\n const env = isRecord(parsed.env) ? parsed.env : undefined;\n const view: WorkflowRunView = {\n runKey,\n workspace,\n displayName: asOptionalString(parsed.displayName) ?? runKey,\n ...(asOptionalString(parsed.workflowName) === undefined\n ? {}\n : { workflowName: asOptionalString(parsed.workflowName) }),\n workflowPath,\n stage: stage as WorkflowStage,\n ...(outcome !== undefined && OUTCOMES.has(outcome) ? { outcome: outcome as WorkflowOutcome } : {}),\n ...(executionState !== undefined && EXECUTION_STATES.has(executionState)\n ? { executionState: executionState as WorkflowRunView['executionState'] }\n : {}),\n ...(asOptionalString(parsed.prompt) === undefined ? {} : { prompt: asOptionalString(parsed.prompt) }),\n startedAt,\n ...(asOptionalString(parsed.finishedAt) === undefined ? {} : { finishedAt: asOptionalString(parsed.finishedAt) }),\n ...(rawError === undefined ? {} : { errorMessage: stripAnsi(rawError) }),\n ...(asOptionalString(parsed.failedJob) === undefined ? {} : { failedJob: asOptionalString(parsed.failedJob) }),\n ...(parsed.stale === true ? { stale: true } : {}),\n ...(asOptionalString(parsed.staleReason) === undefined\n ? {}\n : { staleReason: asOptionalString(parsed.staleReason) }),\n ...(asOptionalString(parsed.worktreeBranch) === undefined\n ? {}\n : { worktreeBranch: asOptionalString(parsed.worktreeBranch) }),\n jobs: [],\n };\n const piSessionId = env === undefined ? undefined : asOptionalString(env[PI_SESSION_ENV]);\n const originalRepoPath = asOptionalString(parsed.originalRepoPath);\n return {\n view,\n ...(piSessionId === undefined ? {} : { piSessionId }),\n ...(originalRepoPath === undefined ? {} : { originalRepoPath }),\n };\n}\n\ninterface WorkflowProgressEvent {\n type: 'job' | 'step';\n status: WorkflowProgressState;\n job: string;\n step?: string;\n index?: number;\n total?: number;\n reason?: string;\n at: string;\n}\n\n/**\n * Parses the append-only progress log. Malformed lines are skipped rather\n * than failing the file: a reader routinely catches a torn final line\n * mid-append.\n */\nexport function parseWorkflowProgress(raw: string): WorkflowProgressEvent[] {\n const events: WorkflowProgressEvent[] = [];\n for (const line of raw.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '') continue;\n let parsed: unknown;\n try {\n parsed = JSON.parse(trimmed);\n } catch {\n continue;\n }\n if (!isRecord(parsed)) continue;\n const type = parsed.type;\n const status = asOptionalString(parsed.status);\n const job = asOptionalString(parsed.job);\n const at = asOptionalString(parsed.at);\n if ((type !== 'job' && type !== 'step') || !job || !at) continue;\n if (status === undefined || !PROGRESS_STATES.has(status)) continue;\n const step = asOptionalString(parsed.step);\n const index = asOptionalNumber(parsed.index);\n const total = asOptionalNumber(parsed.total);\n const reason = asOptionalString(parsed.reason);\n events.push({\n type,\n status: status as WorkflowProgressState,\n job,\n ...(step === undefined ? {} : { step }),\n ...(index === undefined ? {} : { index }),\n ...(total === undefined ? {} : { total }),\n ...(reason === undefined ? {} : { reason }),\n at,\n });\n }\n return events;\n}\n\nfunction jobPhase(name: string): WorkflowJobPhase {\n if (name === PHASE_JOB_PRE) return PHASE_JOB_PRE;\n if (name === PHASE_JOB_POST) return PHASE_JOB_POST;\n return 'job';\n}\n\n/**\n * Folds the event log into the current job tree; later events win, so a job\n * that started and later failed reads as failed. Jobs and steps keep the\n * order of their first appearance.\n */\nexport function foldWorkflowProgress(events: readonly WorkflowProgressEvent[]): WorkflowJobView[] {\n const jobs: WorkflowJobView[] = [];\n const jobByName = new Map<string, WorkflowJobView>();\n for (const event of events) {\n let job = jobByName.get(event.job);\n if (job === undefined) {\n job = { name: event.job, phase: jobPhase(event.job), status: event.status, steps: [] };\n jobByName.set(event.job, job);\n jobs.push(job);\n }\n if (event.type === 'job') {\n job.status = event.status;\n if (event.reason !== undefined) job.reason = event.reason;\n if (event.index !== undefined) job.index = event.index;\n if (event.total !== undefined) job.total = event.total;\n if (event.status === 'running' && job.startedAt === undefined) job.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) job.endedAt = event.at;\n continue;\n }\n if (event.step === undefined) continue;\n let step: WorkflowStepView | undefined = job.steps.find((candidate) => candidate.name === event.step);\n if (step === undefined) {\n step = { name: event.step, status: event.status };\n job.steps.push(step);\n }\n step.status = event.status;\n if (event.reason !== undefined) step.reason = event.reason;\n if (event.status === 'running' && step.startedAt === undefined) step.startedAt = event.at;\n if (STEP_TERMINAL_STATES.has(event.status)) step.endedAt = event.at;\n }\n return jobs;\n}\n\nconst ACTIVE_PROGRESS_STATES: ReadonlySet<WorkflowProgressState> = new Set([\n 'running',\n 'pause_requested',\n 'paused',\n 'resumed',\n]);\n\n/** The job and step a run is on right now, for the NOW breadcrumb. */\nexport function workflowPosition(jobs: readonly WorkflowJobView[]): WorkflowPosition | undefined {\n for (let jobIndex = jobs.length - 1; jobIndex >= 0; jobIndex -= 1) {\n const job = jobs[jobIndex]!;\n if (!ACTIVE_PROGRESS_STATES.has(job.status)) continue;\n for (let stepIndex = job.steps.length - 1; stepIndex >= 0; stepIndex -= 1) {\n const step = job.steps[stepIndex]!;\n if (ACTIVE_PROGRESS_STATES.has(step.status)) {\n return {\n job: job.name,\n step: step.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n }\n return {\n job: job.name,\n ...(job.index === undefined ? {} : { index: job.index }),\n ...(job.total === undefined ? {} : { total: job.total }),\n };\n }\n return undefined;\n}\n\n/** The finished view: record fields plus the folded job tree and position. */\nexport function completeWorkflowRunView(view: WorkflowRunView, jobs: WorkflowJobView[]): WorkflowRunView {\n const position = view.stage === 'running' ? workflowPosition(jobs) : undefined;\n return { ...view, jobs, ...(position === undefined ? {} : { position }) };\n}\n\nexport interface WorkflowScopeInput {\n sessionId: string;\n /** The session's working directory; repo-local runs belong to it too. */\n cwd: string;\n}\n\nfunction pathsOverlap(left: string, right: string): boolean {\n return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);\n}\n\n/**\n * Whether a run belongs on a session's workflow tab: launched by that Pi\n * session (the same env test doompi-workflow's isSessionRun applies), or\n * launched from the repository the session works in. Containment goes both\n * ways so a session rooted in a subdirectory still sees its repo's runs.\n */\nexport function runBelongsToSession(run: ParsedWorkflowRun, scope: WorkflowScopeInput): boolean {\n if (run.piSessionId !== undefined && run.piSessionId === scope.sessionId) return true;\n if (run.originalRepoPath !== undefined && pathsOverlap(run.originalRepoPath, scope.cwd)) return true;\n return pathsOverlap(run.view.workflowPath, scope.cwd);\n}\n\nfunction parseTime(value: string | undefined): number {\n if (value === undefined) return 0;\n const time = Date.parse(value);\n return Number.isFinite(time) ? time : 0;\n}\n\nconst STAGE_ORDER: Readonly<Record<WorkflowStage, number>> = { running: 0, error: 1, completed: 2 };\n\n/**\n * Whether a run still belongs in the view.\n *\n * Only errored runs age out here, and slowly: everything else a session\n * started stays until the engine forgets the run itself.\n */\nfunction withinRetention(run: WorkflowRunView, now: number): boolean {\n if (run.stage !== 'error') return true;\n const settledAt = parseTime(run.finishedAt) || parseTime(run.startedAt);\n return now - settledAt < WORKFLOW_ERROR_RETENTION_MS;\n}\n\n/**\n * When a run last moved, which is what orders it within its group.\n *\n * A running run has only its start; a settled one is ordered by when it\n * settled, because a list kept for the whole session is read as a history and\n * two runs started together can finish an hour apart.\n */\nfunction lastMovedAt(run: WorkflowRunView): number {\n return run.stage === 'running' ? parseTime(run.startedAt) : parseTime(run.finishedAt) || parseTime(run.startedAt);\n}\n\n/**\n * Running runs first, then errored (recovery acts on them), then finished;\n * newest first within a group, and each group capped on its own.\n */\nexport function presentWorkflowRuns(runs: readonly WorkflowRunView[], now: number): WorkflowRunView[] {\n const groups = new Map<WorkflowStage, WorkflowRunView[]>();\n for (const run of runs) {\n if (!withinRetention(run, now)) continue;\n const group = groups.get(run.stage);\n if (group) group.push(run);\n else groups.set(run.stage, [run]);\n }\n return [...groups.entries()]\n .sort(([left], [right]) => STAGE_ORDER[left] - STAGE_ORDER[right])\n .flatMap(([, group]) =>\n group\n .sort((left, right) => lastMovedAt(right) - lastMovedAt(left))\n .slice(0, MAX_PRESENTED_WORKFLOW_RUNS_PER_GROUP),\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ const e=`Another reader holds the keyboard for this run.`;function t(t){let n=t.refreshMs??750,r=t.leaseMs??6e4,i=new Map,a=new Map,o=e=>{let n=a.get(e);if(n!==void 0){if(n.expiresAt>t.now())return n;a.delete(e)}};return{async screen(e,r,a){let o=i.get(e)??{lines:[]};if(i.set(e,o),o.inFlight||o.at!==void 0&&t.now()-o.at<n)return o.lines;o.inFlight=t.terminal.screen(r,{lines:a});try{o.lines=await o.inFlight}finally{o.at=t.now(),o.inFlight=void 0}return o.lines},capabilities(e){return t.terminal.capabilities(e)},takeControl(e,n){let i=o(e);return i!==void 0&&i.token!==n?!1:(a.set(e,{token:n,expiresAt:t.now()+r}),!0)},releaseControl(e,t){o(e)?.token===t&&a.delete(e)},async write(n,i,s,c){let l=o(n);if(l===void 0||l.token!==s)throw Error(e);a.set(n,{token:s,expiresAt:t.now()+r}),await t.terminal.write(i,c)},async resize(n,r,i,a,s){let c=o(n);if(c===void 0||c.token!==i)throw Error(e);return t.terminal.resize(r,a,s)},forget(e){for(let t of i.keys())e.has(t)||i.delete(t);for(let t of a.keys())e.has(t)||a.delete(t)}}}exports.createWorkflowTerminalService=t;
2
+ //# sourceMappingURL=workflowTerminal.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowTerminal.cjs","names":[],"sources":["../../src/services/workflowTerminal.ts"],"sourcesContent":["import type { WorkflowTerminalCapabilitiesView } from '../types/webWorkflowTerminal.ts';\n\n/**\n * One run's terminal, shared by every surface watching it.\n *\n * WHY COALESCING LIVES HERE:\n * Reading a multiplexer's screen forks a CLI, measured at roughly 75ms a call.\n * Several readers on one run (a step tab, a second browser, the session's own\n * panel) would otherwise each fork on their own timer, and a read that runs\n * long would have the next one start on top of it, unbounded. One read per run\n * at a time, no faster than the interval, however many surfaces are watching.\n *\n * WHY A CONTROL LEASE:\n * A run's terminal takes keystrokes from anyone who can reach it, so two\n * readers typing into one nested agent is a real state, not a hypothetical.\n * The lease does not make it impossible, since the session's own panel can\n * still type; it makes it visible, and keeps two cockpit tabs from fighting.\n */\n\n/** The engine's terminal facade, as this service needs it. */\nexport interface TerminalPort<Record> {\n capabilities(record: Record): WorkflowTerminalCapabilitiesView;\n screen(record: Record, options: { lines?: number }): Promise<string[]>;\n write(record: Record, data: string): Promise<void>;\n resize(record: Record, columns: number, rows: number): Promise<boolean>;\n}\n\nexport interface WorkflowTerminalDeps<Record> {\n terminal: TerminalPort<Record>;\n /** Injected so the cadence and the lease clock are assertable. */\n now: () => number;\n /** Shortest gap between two reads of one run. */\n refreshMs?: number;\n /** How long a keyboard lease survives without being renewed. */\n leaseMs?: number;\n}\n\n/** A read in flight, and what the last one returned. */\ninterface ScreenEntry {\n /**\n * When the last read finished, absent until one has.\n *\n * Absent rather than zero: zero is a real instant, and a service whose clock\n * starts near it would answer the very first read from an empty cache and\n * paint nothing until the interval had passed.\n */\n at?: number;\n lines: string[];\n inFlight?: Promise<string[]>;\n}\n\ninterface Lease {\n token: string;\n expiresAt: number;\n}\n\nconst DEFAULT_REFRESH_MS = 750;\nconst DEFAULT_LEASE_MS = 60_000;\nconst NOT_HELD = 'Another reader holds the keyboard for this run.';\n\nexport interface WorkflowTerminalService<Record> {\n /** The run's recent screen, reusing a recent read rather than forking again. */\n screen(identity: string, record: Record, lines: number): Promise<string[]>;\n capabilities(record: Record): WorkflowTerminalCapabilitiesView;\n /** Takes the keyboard, or reports who has it. Renews when the holder asks again. */\n takeControl(identity: string, token: string): boolean;\n releaseControl(identity: string, token: string): void;\n /** Writes on behalf of a lease holder; throws when the lease is not theirs. */\n write(identity: string, record: Record, token: string, data: string): Promise<void>;\n /**\n * Matches the run's terminal to the holder's viewport.\n *\n * Behind the same lease as writing: geometry is shared by everyone watching,\n * so a passive reader must not reflow the screen under the person typing.\n */\n resize(identity: string, record: Record, token: string, columns: number, rows: number): Promise<boolean>;\n /** Drops what is remembered for runs that can never change again. */\n forget(live: ReadonlySet<string>): void;\n}\n\nexport function createWorkflowTerminalService<Record>(\n deps: WorkflowTerminalDeps<Record>,\n): WorkflowTerminalService<Record> {\n const refreshMs = deps.refreshMs ?? DEFAULT_REFRESH_MS;\n const leaseMs = deps.leaseMs ?? DEFAULT_LEASE_MS;\n const screens = new Map<string, ScreenEntry>();\n const leases = new Map<string, Lease>();\n\n const heldBy = (identity: string): Lease | undefined => {\n const lease = leases.get(identity);\n if (lease === undefined) return undefined;\n if (lease.expiresAt > deps.now()) return lease;\n leases.delete(identity);\n return undefined;\n };\n\n return {\n async screen(identity, record, lines) {\n const entry = screens.get(identity) ?? { lines: [] };\n screens.set(identity, entry);\n if (entry.inFlight) return entry.lines;\n if (entry.at !== undefined && deps.now() - entry.at < refreshMs) return entry.lines;\n entry.inFlight = deps.terminal.screen(record, { lines });\n try {\n entry.lines = await entry.inFlight;\n } finally {\n // Stamped after the read, not before: the interval is a gap between\n // reads, so a slow scrape must not immediately earn another one.\n entry.at = deps.now();\n entry.inFlight = undefined;\n }\n return entry.lines;\n },\n capabilities(record) {\n return deps.terminal.capabilities(record);\n },\n takeControl(identity, token) {\n const holder = heldBy(identity);\n if (holder !== undefined && holder.token !== token) return false;\n leases.set(identity, { token, expiresAt: deps.now() + leaseMs });\n return true;\n },\n releaseControl(identity, token) {\n if (heldBy(identity)?.token === token) leases.delete(identity);\n },\n async write(identity, record, token, data) {\n const holder = heldBy(identity);\n if (holder === undefined || holder.token !== token) throw new Error(NOT_HELD);\n leases.set(identity, { token, expiresAt: deps.now() + leaseMs });\n await deps.terminal.write(record, data);\n },\n async resize(identity, record, token, columns, rows) {\n const holder = heldBy(identity);\n if (holder === undefined || holder.token !== token) throw new Error(NOT_HELD);\n return deps.terminal.resize(record, columns, rows);\n },\n forget(live) {\n for (const identity of screens.keys()) {\n if (!live.has(identity)) screens.delete(identity);\n }\n for (const identity of leases.keys()) {\n if (!live.has(identity)) leases.delete(identity);\n }\n },\n };\n}\n"],"mappings":"AAwDA,MAEM,EAAW,kDAsBjB,SAAgB,EACd,EACiC,CACjC,IAAM,EAAY,EAAK,WAAa,IAC9B,EAAU,EAAK,SAAW,IAC1B,EAAU,IAAI,IACd,EAAS,IAAI,IAEb,EAAU,GAAwC,CACtD,IAAM,EAAQ,EAAO,IAAI,CAAQ,EAC7B,OAAU,IAAA,GACd,IAAI,EAAM,UAAY,EAAK,IAAI,EAAG,OAAO,EACzC,EAAO,OAAO,CAAQ,CADmB,CAG3C,EAEA,MAAO,CACL,MAAM,OAAO,EAAU,EAAQ,EAAO,CACpC,IAAM,EAAQ,EAAQ,IAAI,CAAQ,GAAK,CAAE,MAAO,CAAC,CAAE,EAGnD,GAFA,EAAQ,IAAI,EAAU,CAAK,EACvB,EAAM,UACN,EAAM,KAAO,IAAA,IAAa,EAAK,IAAI,EAAI,EAAM,GAAK,EAAW,OAAO,EAAM,MAC9E,EAAM,SAAW,EAAK,SAAS,OAAO,EAAQ,CAAE,OAAM,CAAC,EACvD,GAAI,CACF,EAAM,MAAQ,MAAM,EAAM,QAC5B,QAAU,CAGR,EAAM,GAAK,EAAK,IAAI,EACpB,EAAM,SAAW,IAAA,EACnB,CACA,OAAO,EAAM,KACf,EACA,aAAa,EAAQ,CACnB,OAAO,EAAK,SAAS,aAAa,CAAM,CAC1C,EACA,YAAY,EAAU,EAAO,CAC3B,IAAM,EAAS,EAAO,CAAQ,EAG9B,OAFI,IAAW,IAAA,IAAa,EAAO,QAAU,EAAc,IAC3D,EAAO,IAAI,EAAU,CAAE,QAAO,UAAW,EAAK,IAAI,EAAI,CAAQ,CAAC,EACxD,GACT,EACA,eAAe,EAAU,EAAO,CAC1B,EAAO,CAAQ,CAAC,EAAE,QAAU,GAAO,EAAO,OAAO,CAAQ,CAC/D,EACA,MAAM,MAAM,EAAU,EAAQ,EAAO,EAAM,CACzC,IAAM,EAAS,EAAO,CAAQ,EAC9B,GAAI,IAAW,IAAA,IAAa,EAAO,QAAU,EAAO,MAAU,MAAM,CAAQ,EAC5E,EAAO,IAAI,EAAU,CAAE,QAAO,UAAW,EAAK,IAAI,EAAI,CAAQ,CAAC,EAC/D,MAAM,EAAK,SAAS,MAAM,EAAQ,CAAI,CACxC,EACA,MAAM,OAAO,EAAU,EAAQ,EAAO,EAAS,EAAM,CACnD,IAAM,EAAS,EAAO,CAAQ,EAC9B,GAAI,IAAW,IAAA,IAAa,EAAO,QAAU,EAAO,MAAU,MAAM,CAAQ,EAC5E,OAAO,EAAK,SAAS,OAAO,EAAQ,EAAS,CAAI,CACnD,EACA,OAAO,EAAM,CACX,IAAK,IAAM,KAAY,EAAQ,KAAK,EAC7B,EAAK,IAAI,CAAQ,GAAG,EAAQ,OAAO,CAAQ,EAElD,IAAK,IAAM,KAAY,EAAO,KAAK,EAC5B,EAAK,IAAI,CAAQ,GAAG,EAAO,OAAO,CAAQ,CAEnD,CACF,CACF"}
@@ -0,0 +1,2 @@
1
+ const e=`Another reader holds the keyboard for this run.`;function t(t){let n=t.refreshMs??750,r=t.leaseMs??6e4,i=new Map,a=new Map,o=e=>{let n=a.get(e);if(n!==void 0){if(n.expiresAt>t.now())return n;a.delete(e)}};return{async screen(e,r,a){let o=i.get(e)??{lines:[]};if(i.set(e,o),o.inFlight||o.at!==void 0&&t.now()-o.at<n)return o.lines;o.inFlight=t.terminal.screen(r,{lines:a});try{o.lines=await o.inFlight}finally{o.at=t.now(),o.inFlight=void 0}return o.lines},capabilities(e){return t.terminal.capabilities(e)},takeControl(e,n){let i=o(e);return i!==void 0&&i.token!==n?!1:(a.set(e,{token:n,expiresAt:t.now()+r}),!0)},releaseControl(e,t){o(e)?.token===t&&a.delete(e)},async write(n,i,s,c){let l=o(n);if(l===void 0||l.token!==s)throw Error(e);a.set(n,{token:s,expiresAt:t.now()+r}),await t.terminal.write(i,c)},async resize(n,r,i,a,s){let c=o(n);if(c===void 0||c.token!==i)throw Error(e);return t.terminal.resize(r,a,s)},forget(e){for(let t of i.keys())e.has(t)||i.delete(t);for(let t of a.keys())e.has(t)||a.delete(t)}}}export{t as createWorkflowTerminalService};
2
+ //# sourceMappingURL=workflowTerminal.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflowTerminal.mjs","names":[],"sources":["../../src/services/workflowTerminal.ts"],"sourcesContent":["import type { WorkflowTerminalCapabilitiesView } from '../types/webWorkflowTerminal.ts';\n\n/**\n * One run's terminal, shared by every surface watching it.\n *\n * WHY COALESCING LIVES HERE:\n * Reading a multiplexer's screen forks a CLI, measured at roughly 75ms a call.\n * Several readers on one run (a step tab, a second browser, the session's own\n * panel) would otherwise each fork on their own timer, and a read that runs\n * long would have the next one start on top of it, unbounded. One read per run\n * at a time, no faster than the interval, however many surfaces are watching.\n *\n * WHY A CONTROL LEASE:\n * A run's terminal takes keystrokes from anyone who can reach it, so two\n * readers typing into one nested agent is a real state, not a hypothetical.\n * The lease does not make it impossible, since the session's own panel can\n * still type; it makes it visible, and keeps two cockpit tabs from fighting.\n */\n\n/** The engine's terminal facade, as this service needs it. */\nexport interface TerminalPort<Record> {\n capabilities(record: Record): WorkflowTerminalCapabilitiesView;\n screen(record: Record, options: { lines?: number }): Promise<string[]>;\n write(record: Record, data: string): Promise<void>;\n resize(record: Record, columns: number, rows: number): Promise<boolean>;\n}\n\nexport interface WorkflowTerminalDeps<Record> {\n terminal: TerminalPort<Record>;\n /** Injected so the cadence and the lease clock are assertable. */\n now: () => number;\n /** Shortest gap between two reads of one run. */\n refreshMs?: number;\n /** How long a keyboard lease survives without being renewed. */\n leaseMs?: number;\n}\n\n/** A read in flight, and what the last one returned. */\ninterface ScreenEntry {\n /**\n * When the last read finished, absent until one has.\n *\n * Absent rather than zero: zero is a real instant, and a service whose clock\n * starts near it would answer the very first read from an empty cache and\n * paint nothing until the interval had passed.\n */\n at?: number;\n lines: string[];\n inFlight?: Promise<string[]>;\n}\n\ninterface Lease {\n token: string;\n expiresAt: number;\n}\n\nconst DEFAULT_REFRESH_MS = 750;\nconst DEFAULT_LEASE_MS = 60_000;\nconst NOT_HELD = 'Another reader holds the keyboard for this run.';\n\nexport interface WorkflowTerminalService<Record> {\n /** The run's recent screen, reusing a recent read rather than forking again. */\n screen(identity: string, record: Record, lines: number): Promise<string[]>;\n capabilities(record: Record): WorkflowTerminalCapabilitiesView;\n /** Takes the keyboard, or reports who has it. Renews when the holder asks again. */\n takeControl(identity: string, token: string): boolean;\n releaseControl(identity: string, token: string): void;\n /** Writes on behalf of a lease holder; throws when the lease is not theirs. */\n write(identity: string, record: Record, token: string, data: string): Promise<void>;\n /**\n * Matches the run's terminal to the holder's viewport.\n *\n * Behind the same lease as writing: geometry is shared by everyone watching,\n * so a passive reader must not reflow the screen under the person typing.\n */\n resize(identity: string, record: Record, token: string, columns: number, rows: number): Promise<boolean>;\n /** Drops what is remembered for runs that can never change again. */\n forget(live: ReadonlySet<string>): void;\n}\n\nexport function createWorkflowTerminalService<Record>(\n deps: WorkflowTerminalDeps<Record>,\n): WorkflowTerminalService<Record> {\n const refreshMs = deps.refreshMs ?? DEFAULT_REFRESH_MS;\n const leaseMs = deps.leaseMs ?? DEFAULT_LEASE_MS;\n const screens = new Map<string, ScreenEntry>();\n const leases = new Map<string, Lease>();\n\n const heldBy = (identity: string): Lease | undefined => {\n const lease = leases.get(identity);\n if (lease === undefined) return undefined;\n if (lease.expiresAt > deps.now()) return lease;\n leases.delete(identity);\n return undefined;\n };\n\n return {\n async screen(identity, record, lines) {\n const entry = screens.get(identity) ?? { lines: [] };\n screens.set(identity, entry);\n if (entry.inFlight) return entry.lines;\n if (entry.at !== undefined && deps.now() - entry.at < refreshMs) return entry.lines;\n entry.inFlight = deps.terminal.screen(record, { lines });\n try {\n entry.lines = await entry.inFlight;\n } finally {\n // Stamped after the read, not before: the interval is a gap between\n // reads, so a slow scrape must not immediately earn another one.\n entry.at = deps.now();\n entry.inFlight = undefined;\n }\n return entry.lines;\n },\n capabilities(record) {\n return deps.terminal.capabilities(record);\n },\n takeControl(identity, token) {\n const holder = heldBy(identity);\n if (holder !== undefined && holder.token !== token) return false;\n leases.set(identity, { token, expiresAt: deps.now() + leaseMs });\n return true;\n },\n releaseControl(identity, token) {\n if (heldBy(identity)?.token === token) leases.delete(identity);\n },\n async write(identity, record, token, data) {\n const holder = heldBy(identity);\n if (holder === undefined || holder.token !== token) throw new Error(NOT_HELD);\n leases.set(identity, { token, expiresAt: deps.now() + leaseMs });\n await deps.terminal.write(record, data);\n },\n async resize(identity, record, token, columns, rows) {\n const holder = heldBy(identity);\n if (holder === undefined || holder.token !== token) throw new Error(NOT_HELD);\n return deps.terminal.resize(record, columns, rows);\n },\n forget(live) {\n for (const identity of screens.keys()) {\n if (!live.has(identity)) screens.delete(identity);\n }\n for (const identity of leases.keys()) {\n if (!live.has(identity)) leases.delete(identity);\n }\n },\n };\n}\n"],"mappings":"AAwDA,MAEM,EAAW,kDAsBjB,SAAgB,EACd,EACiC,CACjC,IAAM,EAAY,EAAK,WAAa,IAC9B,EAAU,EAAK,SAAW,IAC1B,EAAU,IAAI,IACd,EAAS,IAAI,IAEb,EAAU,GAAwC,CACtD,IAAM,EAAQ,EAAO,IAAI,CAAQ,EAC7B,OAAU,IAAA,GACd,IAAI,EAAM,UAAY,EAAK,IAAI,EAAG,OAAO,EACzC,EAAO,OAAO,CAAQ,CADmB,CAG3C,EAEA,MAAO,CACL,MAAM,OAAO,EAAU,EAAQ,EAAO,CACpC,IAAM,EAAQ,EAAQ,IAAI,CAAQ,GAAK,CAAE,MAAO,CAAC,CAAE,EAGnD,GAFA,EAAQ,IAAI,EAAU,CAAK,EACvB,EAAM,UACN,EAAM,KAAO,IAAA,IAAa,EAAK,IAAI,EAAI,EAAM,GAAK,EAAW,OAAO,EAAM,MAC9E,EAAM,SAAW,EAAK,SAAS,OAAO,EAAQ,CAAE,OAAM,CAAC,EACvD,GAAI,CACF,EAAM,MAAQ,MAAM,EAAM,QAC5B,QAAU,CAGR,EAAM,GAAK,EAAK,IAAI,EACpB,EAAM,SAAW,IAAA,EACnB,CACA,OAAO,EAAM,KACf,EACA,aAAa,EAAQ,CACnB,OAAO,EAAK,SAAS,aAAa,CAAM,CAC1C,EACA,YAAY,EAAU,EAAO,CAC3B,IAAM,EAAS,EAAO,CAAQ,EAG9B,OAFI,IAAW,IAAA,IAAa,EAAO,QAAU,EAAc,IAC3D,EAAO,IAAI,EAAU,CAAE,QAAO,UAAW,EAAK,IAAI,EAAI,CAAQ,CAAC,EACxD,GACT,EACA,eAAe,EAAU,EAAO,CAC1B,EAAO,CAAQ,CAAC,EAAE,QAAU,GAAO,EAAO,OAAO,CAAQ,CAC/D,EACA,MAAM,MAAM,EAAU,EAAQ,EAAO,EAAM,CACzC,IAAM,EAAS,EAAO,CAAQ,EAC9B,GAAI,IAAW,IAAA,IAAa,EAAO,QAAU,EAAO,MAAU,MAAM,CAAQ,EAC5E,EAAO,IAAI,EAAU,CAAE,QAAO,UAAW,EAAK,IAAI,EAAI,CAAQ,CAAC,EAC/D,MAAM,EAAK,SAAS,MAAM,EAAQ,CAAI,CACxC,EACA,MAAM,OAAO,EAAU,EAAQ,EAAO,EAAS,EAAM,CACnD,IAAM,EAAS,EAAO,CAAQ,EAC9B,GAAI,IAAW,IAAA,IAAa,EAAO,QAAU,EAAO,MAAU,MAAM,CAAQ,EAC5E,OAAO,EAAK,SAAS,OAAO,EAAQ,EAAS,CAAI,CACnD,EACA,OAAO,EAAM,CACX,IAAK,IAAM,KAAY,EAAQ,KAAK,EAC7B,EAAK,IAAI,CAAQ,GAAG,EAAQ,OAAO,CAAQ,EAElD,IAAK,IAAM,KAAY,EAAO,KAAK,EAC5B,EAAK,IAAI,CAAQ,GAAG,EAAO,OAAO,CAAQ,CAEnD,CACF,CACF"}
@@ -1,2 +1,2 @@
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.runsOn&&i.push(r.fg(`dim`,` runs-on ${t.runsOn}`)),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;
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
2
  //# sourceMappingURL=workflowCatalog.cjs.map
@@ -1 +1 @@
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';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay.ts';\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 runsOn?: 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.runsOn) lines.push(theme.fg('dim', ` runs-on ${job.runsOn}`));\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
+ {"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';\nimport {\n DOOM_FULLSCREEN_UI_OPTIONS,\n DOOM_OVERLAY_ACCENT,\n DoomOverlay,\n type DoomOverlayChrome,\n type DoomOverlayTui,\n} from './doomOverlay.ts';\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 +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,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.runsOn&&i.push(r.fg(`dim`,` runs-on ${t.runsOn}`)),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};
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
2
  //# sourceMappingURL=workflowCatalog.mjs.map