Created
June 19, 2026 20:38
-
-
Save CriDos/b7cddd4662ef7cbbecc4d4e68ae7aa4a to your computer and use it in GitHub Desktop.
patch-kilo-autoscroll.ps1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Patch the installed Kilo Code VS Code extension to fix the chat auto-scroll | |
| "can't detach from the latest message" bug. | |
| .DESCRIPTION | |
| Two minimal fixes are applied to the minified webview bundles: | |
| 1. Wheel-up input now records an interaction timestamp so handleScroll's | |
| near-bottom re-attach cannot immediately undo the pause. | |
| 2. The near-bottom re-attach branch no longer clears userScrolled while the | |
| user just scrolled up. | |
| Menu: apply patch, check status, restore backup, quit. | |
| Backups are written as <file>.bak next to each patched bundle. | |
| An extension update overwrites the bundles, so re-apply after updates. | |
| .PARAMETER ExtDir | |
| Optional explicit path to the installed extension folder | |
| (the one containing dist\webview.js). If omitted, the newest | |
| kilocode.kilo-code-* under .vscode\extensions / .vscode-insiders\extensions | |
| is auto-detected. | |
| #> | |
| param( | |
| [string]$ExtDir, | |
| [ValidateSet('apply', 'status', 'restore')] | |
| [string]$Action | |
| ) | |
| $ErrorActionPreference = 'Stop' | |
| # --- patch definitions ------------------------------------------------------- | |
| $fixes = @( | |
| @{ | |
| name = 'wheel-up timestamp' | |
| orig = 'i.deltaY>=0||e.onWheelUp()' | |
| patched = 'i.deltaY>=0||(n=performance.now(),e.onWheelUp())' | |
| }, | |
| @{ | |
| name = 'near-bottom re-attach guard' | |
| orig = 'o.userScrolled&&s("userScrolled",!1);return' | |
| patched = 'o.userScrolled&&!h.isRecent()&&s("userScrolled",!1);return' | |
| } | |
| ) | |
| # --- helpers ---------------------------------------------------------------- | |
| function Find-ExtDir { | |
| if ($ExtDir) { | |
| if (Test-Path -LiteralPath (Join-Path $ExtDir 'dist\webview.js')) { return $ExtDir } | |
| throw "ExtDir '$ExtDir' does not contain dist\webview.js." | |
| } | |
| $roots = @( | |
| (Join-Path $env:USERPROFILE '.vscode\extensions'), | |
| (Join-Path $env:USERPROFILE '.vscode-insiders\extensions') | |
| ) | |
| $found = @() | |
| foreach ($r in $roots) { | |
| if (Test-Path -LiteralPath $r) { | |
| $found += Get-ChildItem -LiteralPath $r -Directory -Filter 'kilocode.kilo-code-*' -ErrorAction SilentlyContinue | |
| } | |
| } | |
| if ($found.Count -eq 0) { throw 'No kilocode.kilo-code-* extension folder found.' } | |
| # newest version folder name wins | |
| return ($found | Sort-Object Name -Descending | Select-Object -First 1).FullName | |
| } | |
| function Get-Targets { | |
| param([string]$dir) | |
| $dist = Join-Path $dir 'dist' | |
| if (!(Test-Path -LiteralPath $dist)) { throw "No dist folder in '$dir'." } | |
| # patch wherever any patch marker (orig or patched) lives | |
| Get-ChildItem -LiteralPath $dist -Filter '*.js' -File | Where-Object { | |
| $c = [IO.File]::ReadAllText($_.FullName) | |
| ($fixes | ForEach-Object { $_.orig, $_.patched } | Where-Object { $c.Contains($_) }).Count -gt 0 | |
| } | |
| } | |
| function Get-FileState { | |
| param([IO.FileInfo]$file) | |
| $c = [IO.File]::ReadAllText($file.FullName) | |
| $state = [ordered]@{ file = $file.Name; orig = 0; patched = 0 } | |
| foreach ($f in $fixes) { | |
| if ($c.Contains($f.orig)) { $state.orig++ } | |
| if ($c.Contains($f.patched)) { $state.patched++ } | |
| } | |
| if ($state.patched -gt 0 -and $state.orig -eq 0) { $state.status = 'patched' } | |
| elseif ($state.patched -gt 0) { $state.status = 'partial' } | |
| elseif ($state.orig -gt 0) { $state.status = 'original' } | |
| else { $state.status = 'n/a' } | |
| return [pscustomobject]$state | |
| } | |
| function Invoke-Apply { | |
| param([IO.FileInfo[]]$targets) | |
| $applied = 0 | |
| foreach ($t in $targets) { | |
| $bak = "$($t.FullName).bak" | |
| if (!(Test-Path -LiteralPath $bak)) { | |
| Copy-Item -LiteralPath $t.FullName -Destination $bak | |
| } | |
| $c = [IO.File]::ReadAllText($t.FullName) | |
| $changed = $false | |
| foreach ($f in $fixes) { | |
| if ($c.Contains($f.patched)) { continue } # already applied | |
| if (!$c.Contains($f.orig)) { continue } # not present in this bundle | |
| $c = $c.Replace($f.orig, $f.patched) | |
| $changed = $true | |
| Write-Host " [$($t.Name)] applied: $($f.name)" | |
| } | |
| if ($changed) { | |
| [IO.File]::WriteAllText($t.FullName, $c) | |
| $applied++ | |
| } else { | |
| Write-Host " [$($t.Name)] already patched or no pattern - skipped" | |
| } | |
| } | |
| if ($applied -gt 0) { | |
| Write-Host '' | |
| Write-Host 'Done. Reload the VS Code window for the change to take effect' -ForegroundColor Green | |
| Write-Host '(Developer: Reload Window, or close and reopen).' | |
| } | |
| } | |
| function Invoke-Restore { | |
| param([IO.FileInfo[]]$targets) | |
| $restored = 0 | |
| foreach ($t in $targets) { | |
| $bak = "$($t.FullName).bak" | |
| if (Test-Path -LiteralPath $bak) { | |
| Copy-Item -LiteralPath $bak -Destination $t.FullName -Force | |
| Write-Host " [$($t.Name)] restored from backup" | |
| $restored++ | |
| } else { | |
| Write-Host " [$($t.Name)] no backup found - skipped" | |
| } | |
| } | |
| if ($restored -gt 0) { | |
| Write-Host '' | |
| Write-Host 'Restored. Reload the VS Code window.' -ForegroundColor Green | |
| } | |
| } | |
| function Show-Status { | |
| param([IO.FileInfo[]]$targets) | |
| if ($targets.Count -eq 0) { | |
| Write-Host 'No patchable bundles found in this extension.' -ForegroundColor Yellow | |
| return | |
| } | |
| $rows = foreach ($t in $targets) { | |
| $s = Get-FileState -file $t | |
| $color = switch ($s.status) { 'patched' { 'Green' } 'original' { 'Yellow' } 'partial' { 'Red' } default { 'DarkGray' } } | |
| [pscustomobject]@{ | |
| File = $s.file | |
| Status = $s.status | |
| Orig = $s.orig | |
| Patched = $s.patched | |
| } | |
| } | |
| $rows | Format-Table -AutoSize | |
| $summary = ($rows | Group-Object Status | ForEach-Object { "$($_.Name):$($_.Count)" }) -join ' ' | |
| Write-Host "Summary: $summary" | |
| } | |
| # --- main menu -------------------------------------------------------------- | |
| try { | |
| $dir = Find-ExtDir | |
| Write-Host "Extension: $dir" -ForegroundColor Cyan | |
| $targets = @(Get-Targets -dir $dir) | |
| if ($targets.Count -eq 0) { | |
| Write-Host 'No bundles containing the auto-scroll pattern were found.' -ForegroundColor Yellow | |
| Write-Host 'The installed version may already be fixed or use different code.' | |
| return | |
| } | |
| Write-Host "Patchable bundles: $($targets.Count)" | |
| Write-Host '' | |
| if ($Action) { | |
| switch ($Action) { | |
| 'apply' { Invoke-Apply -targets $targets } | |
| 'status' { Show-Status -targets $targets } | |
| 'restore' { Invoke-Restore -targets $targets } | |
| } | |
| return | |
| } | |
| while ($true) { | |
| Write-Host '--- Kilo Code auto-scroll patch ---' | |
| Write-Host ' 1) Apply patch' | |
| Write-Host ' 2) Check status' | |
| Write-Host ' 3) Restore from backup' | |
| Write-Host ' 4) Quit' | |
| $choice = Read-Host 'Select [1-4]' | |
| switch ($choice) { | |
| '1' { Invoke-Apply -targets $targets } | |
| '2' { Show-Status -targets $targets } | |
| '3' { Invoke-Restore -targets $targets } | |
| '4' { break } | |
| default { Write-Host 'Invalid choice.' -ForegroundColor Yellow } | |
| } | |
| Write-Host '' | |
| if ($choice -eq '4') { break } | |
| } | |
| } | |
| catch { | |
| Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red | |
| exit 1 | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment