Render GitHub Copilot CLI's Statusline with Oh My Posh
Render GitHub Copilot CLI's Statusline with Oh My Posh
Render GitHub Copilot CLI’s Statusline with Oh My Posh
GitHub Copilot CLI supports a custom statusline command. You can use that hook to run a PowerShell script, map Copilot session data to environment variables, and render the output with Oh My Posh.
This is the working Windows setup I use locally.
Prerequisites
- GitHub Copilot CLI
- PowerShell 7+
- Oh My Posh installed and on
PATH - Nerd Font (recommended for glyphs)
Quick check:
1
2
oh-my-posh version
gh copilot --help
1. Create the Copilot config folder
1
New-Item -ItemType Directory -Force "$env:USERPROFILE\.copilot" | Out-Null
2. Create the statusline command wrapper
Create %USERPROFILE%\.copilot\statusline.cmd:
@echo off
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0statusline.ps1"
3. Create the PowerShell renderer
Create %USERPROFILE%\.copilot\statusline.ps1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()
# Cache directory for performance optimization
$cacheDir = "$env:USERPROFILE\.copilot\.cache"
if (-not (Test-Path $cacheDir)) {
New-Item -ItemType Directory -Force $cacheDir | Out-Null
}
function Format-TokenCount {
param([Nullable[double]]$Value)
if ($null -eq $Value) { return '?' }
if ($Value -ge 1000000) { return ('{0:0.0}m' -f ($Value / 1000000)) }
if ($Value -ge 1000) { return ('{0:0.0}k' -f ($Value / 1000)) }
return ([int]$Value).ToString()
}
function Format-Duration {
param([Nullable[double]]$Milliseconds)
if ($null -eq $Milliseconds -or $Milliseconds -le 0) { return '00:00:00' }
$duration = [TimeSpan]::FromMilliseconds($Milliseconds)
return '{0:00}:{1:00}:{2:00}' -f [int]$duration.TotalHours, $duration.Minutes, $duration.Seconds
}
function New-Gauge {
param([Nullable[double]]$Percent)
if ($null -eq $Percent) { return '..........' }
$bounded = [Math]::Max(0, [Math]::Min(100, [Math]::Round($Percent)))
$filled = [int][Math]::Floor($bounded / 10)
return ('#' * $filled) + ('.' * (10 - $filled))
}
$payload = [Console]::In.ReadToEnd()
# Cache check - return cached output if payload hasn't changed
$payloadHash = (Get-FileHash -InputStream ([System.IO.MemoryStream]::new([System.Text.Encoding]::UTF8.GetBytes($payload))) -Algorithm SHA256).Hash
$cacheFile = Join-Path $cacheDir "payload-$payloadHash.txt"
$cacheOutput = Join-Path $cacheDir "output-$payloadHash.txt"
if ((Test-Path $cacheFile) -and (Test-Path $cacheOutput)) {
Write-Host -NoNewline (Get-Content $cacheOutput)
exit 0
}
try {
$json = $payload | ConvertFrom-Json
} catch {
Write-Host -NoNewline 'Copilot status unavailable'
exit 0
}
$context = $json.context_window
$cost = $json.cost
$currentTokens = if ($null -ne $context.current_context_tokens) {
[double]$context.current_context_tokens
} else {
$null
}
$contextLimit = if ($null -ne $context.displayed_context_limit) {
[double]$context.displayed_context_limit
} else {
$null
}
$contextPercent = if ($null -ne $context.current_context_used_percentage) {
[double]$context.current_context_used_percentage
} elseif ($null -ne $context.used_percentage) {
[double]$context.used_percentage
} else {
$null
}
$linesAdded = if ($null -ne $cost.total_lines_added) { [int]$cost.total_lines_added } else { 0 }
$linesRemoved = if ($null -ne $cost.total_lines_removed) { [int]$cost.total_lines_removed } else { 0 }
$tokenPercent = if ($null -ne $currentTokens -and $null -ne $contextLimit -and $contextLimit -gt 0) {
[Math]::Round(($currentTokens / $contextLimit) * 100)
} else {
0
}
$model = 'GPT-4'
if ($null -ne $json.model) {
if ($json.model -is [string]) {
$model = $json.model
} elseif ($null -ne $json.model.display_name) {
$model = [string]$json.model.display_name
} elseif ($null -ne $json.model.id) {
$model = [string]$json.model.id
} else {
$model = [string]$json.model
}
}
$copilotError = if ($json.error) { $json.error } else { $null }
$status = if ($copilotError) { "ERROR: $copilotError" } else { "ready" }
$fileCount = if ($json.files -is [array]) { $json.files.Count } elseif ($null -ne $json.files) { 1 } else { 0 }
$responseMs = if ($null -ne $json.response_time_ms) { [int]$json.response_time_ms } else { 0 }
$env:COPILOT_STATUS_CONTEXT = "$(Format-TokenCount $currentTokens)/$(Format-TokenCount $contextLimit)"
$env:COPILOT_STATUS_GAUGE = New-Gauge $contextPercent
$env:COPILOT_STATUS_DURATION = Format-Duration $cost.total_duration_ms
$env:COPILOT_STATUS_CHANGES = if ($linesAdded -or $linesRemoved) { "+$linesAdded/-$linesRemoved" } else { '' }
$env:COPILOT_TOKEN_PERCENT = $tokenPercent
$env:COPILOT_MODEL = $model
$env:COPILOT_STATUS = $status
$env:COPILOT_FILE_COUNT = $fileCount
$env:COPILOT_RESPONSE_MS = $responseMs
$theme = Join-Path $PSScriptRoot 'statusline.omp.json'
$cwd = if ($json.cwd) { [string]$json.cwd } else { (Get-Location).Path }
try {
$output = & oh-my-posh print primary --config $theme --pwd $cwd --force --escape=false 2>$null
if ([string]::IsNullOrWhiteSpace($output)) {
throw 'Oh My Posh returned no output.'
}
Set-Content -Path $cacheFile -Value $payload -Force | Out-Null
Set-Content -Path $cacheOutput -Value $output.TrimEnd() -Force | Out-Null
Write-Host -NoNewline $output.TrimEnd()
} catch {
$segments = @(
"🤖 $($env:COPILOT_MODEL)"
"ctx $($env:COPILOT_STATUS_CONTEXT)"
"$($env:COPILOT_STATUS_GAUGE)"
"⏱ $($env:COPILOT_STATUS_DURATION)"
)
if ($env:COPILOT_FILE_COUNT -gt 0) {
$segments += "📄 $($env:COPILOT_FILE_COUNT)"
}
if ($env:COPILOT_STATUS_CHANGES) {
$segments += $env:COPILOT_STATUS_CHANGES
}
if ($env:COPILOT_STATUS -ne 'ready') {
$segments += "⚠️ $($env:COPILOT_STATUS)"
}
$fallback = $segments -join " | "
Set-Content -Path $cacheFile -Value $payload -Force | Out-Null
Set-Content -Path $cacheOutput -Value $fallback -Force | Out-Null
Write-Host -NoNewline $fallback
}
4. Create the statusline theme (plain segments + right-end triangle)
Create %USERPROFILE%\.copilot\statusline.omp.json:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
{
"$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json",
"version": 4,
"final_space": false,
"blocks": [
{
"type": "prompt",
"alignment": "left",
"segments": [
{
"type": "text",
"style": "plain",
"foreground": "#ffffff",
"background": "#c386f1",
"template": " \uf489 Copilot "
},
{
"type": "text",
"style": "plain",
"foreground": "#ffffff",
"background": "#ff479c",
"template": " 🤖 "
},
{
"type": "text",
"style": "plain",
"foreground": "#ffffff",
"background": "#6CA35E",
"template": " \uea83 ctx (%) "
},
{
"type": "text",
"style": "plain",
"foreground": "#193549",
"background": "#fffb38",
"template": " "
},
{
"type": "text",
"style": "plain",
"foreground": "#ffffff",
"background": "#8ED1F7",
"template": " \ueba2 "
},
{
"type": "text",
"style": "plain",
"foreground": "#111111",
"background": "#6CA35E",
"template": " 📄 "
},
{
"type": "text",
"style": "plain",
"foreground": "#111111",
"background": "#FF9248",
"template": " ⚡ ms "
},
{
"type": "text",
"style": "plain",
"foreground": "#ffffff",
"background": "#00897b",
"template": " ✓ ⚠️ "
},
{
"type": "text",
"style": "plain",
"foreground": "#00897b",
"background": "transparent",
"template": "\ue0b0"
}
]
}
]
}
5. Enable statusline in Copilot settings
In %USERPROFILE%\.copilot\settings.json:
1
2
3
4
5
6
7
8
9
10
11
{
"statusLine": {
"enabled": true,
"type": "command",
"command": "%USERPROFILE%\\.copilot\\statusline.cmd",
"padding": 1
},
"feature_flags": {
"enabled": ["STATUS_LINE"]
}
}
If you prefer, you can use an absolute path instead of %USERPROFILE%.
6. Restart Copilot CLI
Close Copilot CLI and launch it again:
1
gh copilot
Troubleshooting
No statusline appears
- Confirm
STATUS_LINEexists infeature_flags.enabled - Confirm
statusLine.enabledistrue - Confirm
statusLine.commandpoints tostatusline.cmd
Broken output
- Run this manually to verify rendering:
oh-my-posh print primary --config "$env:USERPROFILE\.copilot\statusline.omp.json" --force - Confirm script exists:
Test-Path "$env:USERPROFILE\.copilot\statusline.ps1"
Slow rendering
- Clear cache once if needed:
Remove-Item "$env:USERPROFILE\.copilot\.cache\*" -Force -ErrorAction SilentlyContinue
That’s it. You now have a Copilot statusline rendered by Oh My Posh with the same working theme and script used in this post.
This post is licensed under CC BY 4.0 by the author.
