A bunch of PowerShell Universal (PSU) helpers to make your experience more productive.
Reusable Universal Dashboard components for rendering PSU job information, shared across consuming projects (e.g. ComsolCentral, PSUConfig):
New-UDPsuJobHeader- renders a summary header card for a PSU job (script path, status badge, created/started/completed timestamps, duration, copy-to-clipboard job id chip).New-UDPsuJobTerminalView- renders a terminal-style view of a PSU job's output, with an optional structured events table tab, auto-refresh while the job is active, line-number and timestamp toggles, and ANSI-to-HTML color rendering.
Both components rely on private helpers (ConvertFrom-PsuJobOutputEntry, Convert-AnsiToHtml,
Convert-PlainTextToHtml, Get-UDPsuJobThemePalette, Get-UDPsuJobThemeStyleBlock,
ConvertTo-UDPsuThemedIconMarkup) and branding assets under images/synedgy_pwsh/ that ship with
this module -- consuming projects do not need to duplicate them. See
images/synedgy_pwsh/COPYRIGHT.md for the copyright/usage notice covering that artwork.
$job = Get-PSUJob -Id $jobId
New-UDPsuJobHeader -Job $job
New-UDPsuJobTerminalView -JobId $job.Id -JobStatus ([string]$job.Status) `
-JobOutputSnapshot @($job.Output) -IncludeStructuredTableNew-UDPsuJobTerminalView's live-refresh New-UDDynamic content block imports the exact module
path used by the parent dashboard runspace and calls its private helpers from that module scope,
since private module functions are not otherwise accessible from a New-UDDynamic sync/refresh
runspace. This also supports deployments where the module was loaded directly from a path that
is not discoverable through the refresh runspace's PSModulePath.
Pass -JobOutputSnapshot @($job.Output) when the job object is already available. If a refresh
runspace cannot retrieve live output (for example because its connection or authentication
context differs from the parent dashboard runspace), the component displays that snapshot and a
visible warning instead of rendering a blank terminal.
The component also writes failure diagnostics with PSU's built-in Write-PSULog command. View
them under Observe > Logging > Live Logs, filtering Feature to App and Resource to
New-UDPsuJobTerminalView:<ElementId>. Events include the job id, dynamic view, runspace stage,
fallback usage, and exception message. App tokens are redacted from fetch errors, and neither app
tokens nor job-output content are included as structured log properties.
Both components accept a -Theme parameter:
Auto(default): follows PSU's own theme switch live. PSU sets adata-theme="light"ordata-theme="dark"attribute on<html>and updates it instantly (no page reload) when the user toggles its built-in theme switch. The component's colors and icon are driven by CSS custom properties (--psu-term-*) scoped to the component's own element id, defined once in a<style>block emitted alongside the component (seeGet-UDPsuJobThemeStyleBlock), so they re-cascade automatically whenever PSU flips that attribute.Light/Dark: forces that theme for this component instance regardless of the ambient PSU theme (renders only that palette's CSS rule, with nohtml[data-theme="dark"]override rule).
The light palette is inspired by the classic PowerShell ISE / VS Code light theme (white background, dark text, ISE-style syntax accent colors for the Error/Warning/Information/ Verbose/Debug job output streams). The dark palette preserves the component's original look.
Both components also accept a -CustomCss parameter: raw CSS appended after the theme rules in
the same <style> block, so it naturally overrides them. For example, to override just the accent
color for a specific instance:
New-UDPsuJobTerminalView -JobId $job.Id -ElementId 'my-terminal' `
-CustomCss '#my-terminal { --psu-term-accent: #ff6600; }'Icon assets always use the plain monochrome variants: pwsh_custom_black.svg for light theme and
pwsh_custom_white.svg for dark theme. In Auto mode, both variants are rendered (wrapped in
.psu-theme-light-only / .psu-theme-dark-only spans) and shown/hidden via the same CSS rules
that drive the color variables, so the icon also switches live with no JavaScript required.
New-UDPsuJobTerminalView accepts -HideLineNumbers and -HideTimestamps switches to control
whether line numbers and timestamps are shown by default for a first-time viewer (shown by default
when omitted). Once a viewer manually clicks the toggle buttons, their choice is remembered in the
browser's localStorage and takes precedence over these defaults on later visits to the same page.
# Hide both by default for first-time viewers; still toggleable per-viewer afterwards
New-UDPsuJobTerminalView -JobId $job.Id -HideLineNumbers -HideTimestampssynedgy.universal.helper also ships an AiTool class attribute, an [System.Attribute] you can
put on any public function in a consuming module to expose it as a PowerShell Universal AI tool,
mirroring the existing APIEndpoint attribute pattern used for REST endpoints:
function Find-UserByCity
{
[CmdletBinding()]
[AiTool(
Description = 'Finds users by city'
Authenticated = $false
Role = ('admin','user')
)]
param
(
[Parameter(Mandatory = $true)]
[System.String]
$City
)
# ...
}Then, in the consuming module's .universal/*.ps1 resource file, a single call discovers and
registers every [AiTool()]-decorated function in the module:
Import-PSUAiTool -Module PSUConfig -Environment 'PSUConfig'Import-PSUAiTool uses Get-ModuleAiTool to scan the module's exported functions for the
AiTool attribute, then for each one:
- Registers (or re-registers -- both cmdlets are idempotent, safe to call on every PSU config
reload) a backing PSU Script resource via
New-PSUScript -Module <Module> -Command <Function>. - Registers the AI tool itself via
New-PSUAiTool -ScriptFullPath '<Module>\<Function>' ....
This differs from the APIEndpoint/Import-PSUEndpoint pattern: New-PSUEndpoint accepts an
inline -Endpoint {scriptblock}, so Import-PSUEndpoint can synthesize a wrapper scriptblock on
the fly with no separate script registration needed. New-PSUAiTool, however, only accepts
-ScriptFullPath referencing an existing PSU Script resource (the same <Module>\<Command>
naming convention used by New-PSUScript/Get-PSUScript), so Import-PSUAiTool always
(re-)declares that backing script alongside the AI tool registration.
AiTool attribute properties, all optional:
| Property | Default | Description |
|---|---|---|
Name |
function name | Name of the registered AI tool. |
Description |
comment-based help synopsis | Description shown to AI agents. |
Authenticated |
$false |
Whether the tool requires an authenticated user. |
Role |
(none) | Role(s) required to access/invoke the tool. |
Mcp |
$true |
Whether the tool is exposed through Model Context Protocol (MCP). |
Environment |
(none) | PSU environment the backing script should run in. |
When fetching the $job passed into these components, prefer an app token via the Management API
(Get-PSUJob -Id $jobId -AppToken $token -ComputerName $url) over -Integrated. -Integrated
bypasses PSU's Management API -- and therefore its role/permission checks -- entirely, so code
using it can read any job in the instance regardless of role configuration. An app token, by
contrast, is bound to whatever role it is assigned, is revocable, and is auditable. Treat
-Integrated as a fallback for quick local setups without a configured token, not the default.
New-UDPsuJobTerminalView follows the same connection model for output retrieval. When
-UniversalServerUrl is supplied, it passes that value as ComputerName and includes
-AppToken when supplied. When the URL is omitted, it calls Get-PSUJobOutput -Integrated;
the helper module does not discover a URL from Get-ModuleConfig or assume a localhost address.
An app token therefore requires an explicit -UniversalServerUrl.
This does not yet achieve fine-grained per-script job scoping -- today the only permission that
grants job-read access at all (automation/read) grants it for every job in the instance, not
just a specific script's jobs (a known PSU platform limitation, already submitted to Devolutions
and confirmed as being fixed). But using a token still keeps access inside PSU's authorization
model rather than bypassing it altogether, and will become properly scoped once that fix ships.
These are general PSU platform quirks worth knowing when building on top of this module or PSU apps that consume it:
Get-PSUScript -Namerequires the full<ModuleName>\<Command>path (e.g.'PSUConfig\Get-myUser'), not just the command name.Invoke-PSUScript -Integrated -Waitreliably returns a job object with an emptyIdon this PSU version (100% reproducible across 130 test invocations). Omit-Waitwhen the jobIdis needed immediately (e.g. to redirect to a job detail page) -- without-Wait,Idis reliably populated.- Never directly modify files under PSU's live Repository folder
(
C:\ProgramData\UniversalAutomation\Repositoryon Windows); only a build-driven deploy task should change files there. - When deploying a Sampler-built module to PSU, always run the
packtask explicitly beforedeploy-- Invoke-Build's incremental build detection can silently skippack, leaving stale or missing.nupkgfiles and causing PSU deployment to fail without a clear error in the build output.