Set-SpectreColors -AccentColor DeepPink1
# Build root layout scaffolding for:
# +--------------------------------+
# | Title | <- Update-TitleComponent will render the title
# |--------------------------------|
# | | <- Update-MessageListComponent will display the list of messages here
# |--------------------------------|
# | CustomTextEntry | <- Update-CustomTextEntryComponent will create a text entry prompt here that is manually managed by pushing keys into a string
# |________________________________|
$layout = New-SpectreLayout -Name "root" -Rows @(
(New-SpectreLayout -Name "title" -MinimumSize 5 -Ratio 1 -Data ("empty")),
(New-SpectreLayout -Name "messages" -Ratio 10 -Data ("empty")),
(New-SpectreLayout -Name "customTextEntry" -MinimumSize 5 -Ratio 1 -Data ("empty"))
# Component functions for rendering the content of each panel
function Update-TitleComponent {
[Spectre.Console.LiveDisplayContext] $Context,
[Spectre.Console.Layout] $LayoutComponent
("🧠 ChaTTY" | Format-SpectreAligned -HorizontalAlignment Center -VerticalAlignment Middle | Format-SpectrePadded -Padding 1),
(Write-SpectreRule -LineColor DeepPink1 -PassThru)
) | Format-SpectreRows | Format-SpectrePanel -Border None
$LayoutComponent.Update($component) | Out-Null
function Update-MessageListComponent {
[Spectre.Console.LiveDisplayContext] $Context,
[Spectre.Console.Layout] $LayoutComponent,
[System.Collections.Stack] $Messages
foreach ($message in $Messages) {
if ($message.Actor -eq "System") {
$rows += $message.Message.PadRight(6) `
| Get-SpectreEscapedText `
| Write-SpectreHost -Justify Left -PassThru `
| Format-SpectrePanel -Color Grey -Header "System" `
| Format-SpectreAligned -HorizontalAlignment Left `
| Format-SpectrePadded -Top 0 -Left 10 -Bottom 0 -Right 0
$rows += $message.Message.PadRight($message.Actor.Length) `
| Get-SpectreEscapedText `
| Write-SpectreHost -Justify Right -PassThru `
| Format-SpectrePanel -Color Pink1 -Header $message.Actor `
| Format-SpectreAligned -HorizontalAlignment Right `
| Format-SpectrePadded -Top 0 -Left 0 -Bottom 0 -Right 10
# Add the heights of each message until reaching the max size, subtract the height of the title and text entry components (10)
$availableHeight = $Host.UI.RawUI.WindowSize.Height - 10
foreach ($row in $rows) {
$totalHeight += ($row | Get-SpectreRenderableSize).Height
if ($totalHeight -gt $availableHeight) {
# Stack is LIFO, so we need to reverse it to display the messages in the correct order
[array]::Reverse($rowsToRender)
$component = $rowsToRender | Format-SpectreRows | Format-SpectreAligned -VerticalAlignment Top | Format-SpectrePanel -Border None
$LayoutComponent.Update($component) | Out-Null
function Update-CustomTextEntryComponent {
[Spectre.Console.LiveDisplayContext] $Context,
[Spectre.Console.Layout] $LayoutComponent,
$safeInput = [string]::IsNullOrEmpty($CurrentInput) ? "" : ($CurrentInput | Get-SpectreEscapedText)
$component = "[gray]Prompt:[/] $safeInput" | Format-SpectrePanel -Expand | Format-SpectrePadded -Top 0 -Left 20 -Bottom 0 -Right 20 | Format-SpectreAligned -HorizontalAlignment Center
$LayoutComponent.Update($component) | Out-Null
function Get-SomeChatResponse {
[System.Collections.Stack] $Messages,
[Spectre.Console.LiveDisplayContext] $Context,
[Spectre.Console.Layout] $LayoutComponent
for ($i = 0; $i -lt 3; $i++) {
$Messages.Push(@{ Actor = "System"; Message = ("." * $ellipsisCount) })
Update-MessageListComponent -Context $Context -LayoutComponent $LayoutComponent -Messages $Messages
Start-Sleep -Milliseconds 500
# Remove the last thinking message
return @{ Actor = "System"; Message = "I don't understand what you're saying." }
function Get-LastChatKeyPressed {
return [Console]::ReadKey($true)
# Start live rendering the layout
Invoke-SpectreLive -Data $layout -ScriptBlock {
[Spectre.Console.LiveDisplayContext] $Context
$messages = [System.Collections.Stack]::new(@(
@{ Actor = "System"; Message = "👋 Hello, welcome to ChaTTY!" },
@{ Actor = "System"; Message = "Type your message and press Enter to send it." },
@{ Actor = "System"; Message = "Use the Up and Down arrow keys to scroll through previous messages." },
@{ Actor = "System"; Message = "Press 'ctrl-c' to close the chat." }
Update-TitleComponent -Context $Context -LayoutComponent $layout["title"]
Update-MessageListComponent -Context $Context -LayoutComponent $layout["messages"] -Messages $messages
Update-CustomTextEntryComponent -Context $Context -LayoutComponent $layout["customTextEntry"] -CurrentInput $currentInput
# Real basic input handling, just add characters and remove if backspace is pressed, submit message if Enter is pressed
[Console]::TreatControlCAsInput = $true
$lastKeyPressed = Get-LastChatKeyPressed
if ($lastKeyPressed.Key -eq "C" -and $lastKeyPressed.Modifiers -eq "Control") {
# Exit the loop. You have to treat ctrl-c as input to avoid the console readkey blocking the sigint
} elseif ($lastKeyPressed.Key -eq "Enter") {
# Add the latest user message to the message stack
$messages.Push(@{ Actor = ($env:USERNAME + $env:USER); Message = $currentInput })
Update-CustomTextEntryComponent -Context $Context -LayoutComponent $layout["customTextEntry"] -CurrentInput $currentInput
Update-MessageListComponent -Context $Context -LayoutComponent $layout["messages"] -Messages $messages
$messages.Push((Get-SomeChatResponse -Messages $messages -Context $Context -LayoutComponent $layout["messages"]))
} elseif($lastKeyPressed.Key -eq "Backspace") {
# Remove the last character from the current input string
$currentInput = $currentInput.Substring(0, [Math]::Max(0, $currentInput.Length - 1))
} elseif ($lastKeyPressed.KeyChar) {
# Add the character to the current input string
$currentInput += $lastKeyPressed.KeyChar