Skip to content

[FEATURE]: Restore the legacy UI with the persistent left sidebar as an option #48882

Description

@HRronaldo

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

The recent sidebar redesign (#20242) replaced the classic two-panel layout
(persistent left sidebar + session panel) with a Codex-style unified view.
For me this is not a visual refresh — it removed the single most important
part of my daily workflow, and there is currently no way to go back.

What I want changed / added:

  1. Keep the legacy UI with the persistent left sidebar available as an
    option (a legacy / classic UI toggle or config flag is enough — it
    doesn't have to be the default).
  2. If the legacy layout can't be maintained, at minimum restore an
    always-visible left sidebar — not hover-only menus or a collapsed rail
    that requires extra clicks.

Why this matters (the benefit):

I live inside opencode for hours every day. The left sidebar gives me an
at-a-glance, persistent view of my projects, workspaces, and sessions:

  • I can see and switch between projects/contexts without breaking flow.
  • Sessions are one click away, not buried behind hover or search.
  • Context switching stays fast and predictable.

The new interface hides all of this behind a single unified list with
hover/focus interactions. Every session feels slower and more disoriented.
This is not an aesthetic preference — removing the sidebar with no escape
hatch is a functional regression for anyone who relies on it.

Related:

Happy to provide screenshots, my opencode version, and platform details if
that helps. Please don't leave those of us who depend on the left sidebar
behind.

Activity

  1. github-actions commented on Sep 14, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    Please review these issues and consider adding your feedback there instead, or linking them as related.

  2. HRronaldo commented on Sep 14, 2026

    @HRronaldo
    Author
    Image

    win11 opencode desktop
    version:v1.18.30

  3. zhengxingjian commented on Sep 14, 2026

    @zhengxingjian

    准备换deepseek-harness,新布局生产力不行

  4. aa2013 commented on Sep 14, 2026

    @aa2013

    新UI太难用了,完全没用旧UI好用,新UI操作太反人类

  5. 0-SamboNZ-0 commented on Sep 14, 2026

    @0-SamboNZ-0

    I also agree that the original layout was MUCH easier to use; this is a significant loss for usability!

  6. aa2013 commented on Sep 14, 2026

    @aa2013

    写了个powersell脚本,替换opencode打包后的 asar 里面的新布局判断逻辑强制使用旧布局,执行powershell,输入asar的文件夹,通常是:
    C:\Users\%Username%\AppData\Local\Programs\@opencode-aidesktop\resources
    注意替换路径里面的用户名,脚本会自动备份当前的asar,执行完成后重启opencode即可,支持 1.18.30 版本
    注意:每次更新opencode后需要重新执行

    I wrote a PowerShell script that replaces the new layout detection logic inside the packaged asar of opencode, forcing it to use the old layout. Run PowerShell and enter the asar folder, which is usually:
    C:\Users\%Username%\AppData\Local\Programs\@opencode-aidesktop\resources
    Note: replace the username in the path. The script will automatically back up the current asar. After it finishes, just restart opencode. It supports version 1.18.30.
    Note: You need to run it again after each opencode update.

    Image

    script

    generated by ai

    Copy and save it as a .ps1 file, then run it.

    $ErrorActionPreference = "Stop"
    
    # Wait for a key press so the console window does not close before the result is visible.
    function Wait-Key {
        Write-Host ""
        Write-Host "Press any key to exit ..."
        [void][Console]::ReadKey($true)
    }
    
    # ---------- Step 0: locate app.asar ----------
    Write-Host "[1/8] Resolve app.asar path from the folder you provide ..."
    $inputPath = Read-Host "Enter the opencode install folder"
    $inputPath = $inputPath.Trim().Trim('"', "'")
    
    if ([string]::IsNullOrWhiteSpace($inputPath)) {
        Write-Host "Error: path cannot be empty."
        Wait-Key
        exit 1
    }
    
    if (-not (Test-Path -LiteralPath $inputPath -PathType Container)) {
        Write-Host "Error: folder not found: $inputPath"
        Wait-Key
        exit 1
    }
    
    # 只接受文件夹:优先找其下的 resources\app.asar,其次文件夹内直接存在的 app.asar
    $item = Get-Item -LiteralPath $inputPath
    $candidates = @(
        (Join-Path $item.FullName "resources\app.asar"),
        (Join-Path $item.FullName "app.asar")
    )
    $AsarPath = $null
    foreach ($candidate in $candidates) {
        if (Test-Path -LiteralPath $candidate -PathType Leaf) {
            $AsarPath = $candidate
            break
        }
    }
    if (-not $AsarPath) {
        Write-Host "Error: no app.asar found under: $($item.FullName)"
        Write-Host "Expected at 'resources\app.asar' or 'app.asar'."
        Wait-Key
        exit 1
    }
    Write-Host "       app.asar: $AsarPath"
    
    # ---------- Step 1: prepare equal-length replacement ----------
    Write-Host "[2/8] Building equal-length replacement string ..."
    $target = "if (layoutUpgrade()) return true;"
    $replacement = "return false;"
    $pad = " " * ($target.Length - $replacement.Length)
    $newString = $replacement + $pad
    
    # Equal-length invariant must hold, otherwise the ASAR header offsets break.
    if ($newString.Length -ne $target.Length) {
        Write-Host "Error: equal-length invariant broken."
        Wait-Key
        exit 1
    }
    Write-Host "       target len: $($target.Length) / replacement len: $($newString.Length)"
    
    # ---------- Step 2: read bytes ----------
    Write-Host "[3/8] Reading bytes from $AsarPath ..."
    $targetBytes = [System.Text.Encoding]::ASCII.GetBytes($target)
    $newBytes = [System.Text.Encoding]::ASCII.GetBytes($newString)
    $bytes = [System.IO.File]::ReadAllBytes($AsarPath)
    Write-Host "       read $($bytes.Length) bytes"
    
    # Naive scan for a byte sub-sequence; good enough for a short needle.
    function Find-Bytes {
        param(
            [byte[]]$Data,
            [byte[]]$Pattern,
            [int]$StartIndex = 0
        )
        $last = $Data.Length - $Pattern.Length
        for ($i = $StartIndex; $i -le $last; $i++) {
            if ($Data[$i] -ne $Pattern[0]) { continue }
            $ok = $true
            for ($j = 1; $j -lt $Pattern.Length; $j++) {
                if ($Data[$i + $j] -ne $Pattern[$j]) {
                    $ok = $false
                    break
                }
            }
            if ($ok) { return $i }
        }
        return -1
    }
    
    # ---------- Step 3: search for target ----------
    Write-Host "[4/8] Searching for target string ..."
    $index = Find-Bytes -Data $bytes -Pattern $targetBytes
    
    if ($index -lt 0) {
        Write-Host "       Target string not found; maybe already locked."
        Wait-Key
        exit 0
    }
    Write-Host "       found at offset $index"
    
    # Ensure the match is unique so we never patch the wrong spot.
    $second = Find-Bytes -Data $bytes -Pattern $targetBytes -StartIndex ($index + 1)
    if ($second -ne -1) {
        Write-Host "Error: multiple matches found (offset $second); aborting."
        Wait-Key
        exit 1
    }
    
    # ---------- Step 4: backup ----------
    Write-Host "[5/8] Creating backup ..."
    $bakPath = [System.IO.Path]::ChangeExtension($AsarPath, "asar.bak")
    if (-not (Test-Path -LiteralPath $bakPath)) {
        Copy-Item -LiteralPath $AsarPath -Destination $bakPath
        Write-Host "       Backup created: $bakPath"
    }
    else {
        Write-Host "       Backup exists, skip: $bakPath"
    }
    
    # ---------- Step 5: patch in memory ----------
    Write-Host "[6/8] Applying equal-length replacement ..."
    $originalLength = $bytes.Length
    
    # Overwrite the matched region with the equal-length replacement.
    [System.Array]::Copy($newBytes, 0, $bytes, $index, $newBytes.Length)
    
    if ($bytes.Length -ne $originalLength) {
        Write-Host "Error: array length changed; aborting before write."
        Wait-Key
        exit 1
    }
    
    # ---------- Step 6: write back ----------
    Write-Host "[7/8] Writing patched bytes back ..."
    [System.IO.File]::WriteAllBytes($AsarPath, $bytes)
    
    # ---------- Step 7: verify ----------
    Write-Host "[8/8] Verifying patched region ..."
    $verifyBytes = [System.IO.File]::ReadAllBytes($AsarPath)
    $verifySlice = $verifyBytes[$index..($index + $newBytes.Length - 1)]
    $verifyText = [System.Text.Encoding]::ASCII.GetString($verifySlice)
    if (-not $verifyText.StartsWith($replacement)) {
        Write-Host "Error: verification failed; unexpected content after write."
        Wait-Key
        exit 1
    }
    
    Write-Host "Success: old layout locked."
    Wait-Key
  7. JamesLi2013 commented on Sep 14, 2026

    @JamesLi2013

    Keep legacy layout option
    If the new layout is used, I never want to use it again.

  8. diuwu commented on Sep 14, 2026

    @diuwu

    We strongly demand that the old layout be restored. Ever since it was changed to the new layout, my colleagues and I have all given up on opencode.

  9. 0-SamboNZ-0 commented on Sep 14, 2026

    @0-SamboNZ-0

    I wrote a PowerShell script that replaces the new layout detection logic inside the packaged asar of opencode, forcing it to use the old layout.

    Beautiful! Works perfectly! Thank you for sharing!

  10. ellamas-grupoarpada commented on Sep 14, 2026

    @ellamas-grupoarpada

    写了个powersell脚本,替换opencode打包后的 asar 里面的新布局判断逻辑强制使用旧布局,执行powershell,输入asar的文件夹,通常是: C:\Users\%Username%\AppData\Local\Programs\@opencode-aidesktop\resources 注意替换路径里面的用户名,脚本会自动备份当前的asar,执行完成后重启opencode即可,支持 1.18.30 版本 注意:每次更新opencode后需要重新执行

    I wrote a PowerShell script that replaces the new layout detection logic inside the packaged asar of opencode, forcing it to use the old layout. Run PowerShell and enter the asar folder, which is usually: C:\Users\%Username%\AppData\Local\Programs\@opencode-aidesktop\resources Note: replace the username in the path. The script will automatically back up the current asar. After it finishes, just restart opencode. It supports version 1.18.30. Note: You need to run it again after each opencode update.

    Image

    script

    generated by ai

    Copy and save it as a .ps1 file, then run it.

    $ErrorActionPreference = "Stop"

    Wait for a key press so the console window does not close before the result is visible.

    function Wait-Key {
    Write-Host ""
    Write-Host "Press any key to exit ..."
    [void][Console]::ReadKey($true)
    }

    ---------- Step 0: locate app.asar ----------

    Write-Host "[1/8] Resolve app.asar path from the folder you provide ..."
    $inputPath = Read-Host "Enter the opencode install folder"
    $inputPath = $inputPath.Trim().Trim('"', "'")

    if ([string]::IsNullOrWhiteSpace($inputPath)) {
    Write-Host "Error: path cannot be empty."
    Wait-Key
    exit 1
    }

    if (-not (Test-Path -LiteralPath $inputPath -PathType Container)) {
    Write-Host "Error: folder not found: $inputPath"
    Wait-Key
    exit 1
    }

    只接受文件夹:优先找其下的 resources\app.asar,其次文件夹内直接存在的 app.asar

    $item = Get-Item -LiteralPath $inputPath
    $candidates = @(
    (Join-Path $item.FullName "resources\app.asar"),
    (Join-Path $item.FullName "app.asar")
    )
    $AsarPath = $null
    foreach ($candidate in $candidates) {
    if (Test-Path -LiteralPath $candidate -PathType Leaf) {
    $AsarPath = $candidate
    break
    }
    }
    if (-not $AsarPath) {
    Write-Host "Error: no app.asar found under: $($item.FullName)"
    Write-Host "Expected at 'resources\app.asar' or 'app.asar'."
    Wait-Key
    exit 1
    }
    Write-Host " app.asar: $AsarPath"

    ---------- Step 1: prepare equal-length replacement ----------

    Write-Host "[2/8] Building equal-length replacement string ..."
    $target = "if (layoutUpgrade()) return true;"
    $replacement = "return false;"
    $pad = " " * ($target.Length - $replacement.Length)
    $newString = $replacement + $pad

    Equal-length invariant must hold, otherwise the ASAR header offsets break.

    if ($newString.Length -ne $target.Length) {
    Write-Host "Error: equal-length invariant broken."
    Wait-Key
    exit 1
    }
    Write-Host " target len: $($target.Length) / replacement len: $($newString.Length)"

    ---------- Step 2: read bytes ----------

    Write-Host "[3/8] Reading bytes from $AsarPath ..."
    $targetBytes = [System.Text.Encoding]::ASCII.GetBytes($target)
    $newBytes = [System.Text.Encoding]::ASCII.GetBytes($newString)
    $bytes = [System.IO.File]::ReadAllBytes($AsarPath)
    Write-Host " read $($bytes.Length) bytes"

    Naive scan for a byte sub-sequence; good enough for a short needle.

    function Find-Bytes {
    param(
    [byte[]]$Data,
    [byte[]]$Pattern,
    [int]$StartIndex = 0
    )
    $last = $Data.Length - $Pattern.Length
    for ($i = $StartIndex; $i -le $last; $i++) {
    if ($Data[$i] -ne $Pattern[0]) { continue }
    $ok = $true
    for ($j = 1; $j -lt $Pattern.Length; $j++) {
    if ($Data[$i + $j] -ne $Pattern[$j]) {
    $ok = $false
    break
    }
    }
    if ($ok) { return $i }
    }
    return -1
    }

    ---------- Step 3: search for target ----------

    Write-Host "[4/8] Searching for target string ..."
    $index = Find-Bytes -Data $bytes -Pattern $targetBytes

    if ($index -lt 0) {
    Write-Host " Target string not found; maybe already locked."
    Wait-Key
    exit 0
    }
    Write-Host " found at offset $index"

    Ensure the match is unique so we never patch the wrong spot.

    $second = Find-Bytes -Data $bytes -Pattern $targetBytes -StartIndex ($index + 1)
    if ($second -ne -1) {
    Write-Host "Error: multiple matches found (offset $second); aborting."
    Wait-Key
    exit 1
    }

    ---------- Step 4: backup ----------

    Write-Host "[5/8] Creating backup ..."
    $bakPath = [System.IO.Path]::ChangeExtension($AsarPath, "asar.bak")
    if (-not (Test-Path -LiteralPath $bakPath)) {
    Copy-Item -LiteralPath $AsarPath -Destination $bakPath
    Write-Host " Backup created: $bakPath"
    }
    else {
    Write-Host " Backup exists, skip: $bakPath"
    }

    ---------- Step 5: patch in memory ----------

    Write-Host "[6/8] Applying equal-length replacement ..."
    $originalLength = $bytes.Length

    Overwrite the matched region with the equal-length replacement.

    [System.Array]::Copy($newBytes, 0, $bytes, $index, $newBytes.Length)

    if ($bytes.Length -ne $originalLength) {
    Write-Host "Error: array length changed; aborting before write."
    Wait-Key
    exit 1
    }

    ---------- Step 6: write back ----------

    Write-Host "[7/8] Writing patched bytes back ..."
    [System.IO.File]::WriteAllBytes($AsarPath, $bytes)

    ---------- Step 7: verify ----------

    Write-Host "[8/8] Verifying patched region ..."
    $verifyBytes = [System.IO.File]::ReadAllBytes($AsarPath)
    $verifySlice = $verifyBytes[$index..($index + $newBytes.Length - 1)]
    $verifyText = [System.Text.Encoding]::ASCII.GetString($verifySlice)
    if (-not $verifyText.StartsWith($replacement)) {
    Write-Host "Error: verification failed; unexpected content after write."
    Wait-Key
    exit 1
    }

    Write-Host "Success: old layout locked."
    Wait-Key

    Works! Thanks!

    Made opencode check for security, and it added timestamped .bak (just in case of repetition):


    $ErrorActionPreference = "Stop"

    Wait for a key press so the console window does not close before the result is visible.

    function Wait-Key {
    Write-Host ""
    Write-Host "Press any key to exit ..."
    [void][Console]::ReadKey($true)
    }

    function Find-Bytes {
    param(
    [byte[]]$Data,
    [byte[]]$Pattern,
    [int]$StartIndex = 0
    )

    $last = $Data.Length - $Pattern.Length
    for ($i = $StartIndex; $i -le $last; $i++) {
        if ($Data[$i] -ne $Pattern[0]) { continue }
    
        $match = $true
        for ($j = 1; $j -lt $Pattern.Length; $j++) {
            if ($Data[$i + $j] -ne $Pattern[$j]) {
                $match = $false
                break
            }
        }
    
        if ($match) { return $i }
    }
    
    return -1
    

    }

    Write-Host "[1/8] Resolve app.asar path from the folder you provide ..."
    $inputPath = Read-Host "Enter the opencode install folder"
    $inputPath = $inputPath.Trim().Trim([char[]]@([char]34, [char]39))

    if ([string]::IsNullOrWhiteSpace($inputPath)) {
    Write-Host "Error: path cannot be empty."
    Wait-Key
    exit 1
    }

    if (-not (Test-Path -LiteralPath $inputPath -PathType Container)) {
    Write-Host "Error: folder not found: $inputPath"
    Wait-Key
    exit 1
    }

    $item = Get-Item -LiteralPath $inputPath
    $candidates = @(
    (Join-Path $item.FullName "resources\app.asar"),
    (Join-Path $item.FullName "app.asar")
    )

    $asarPath = $null
    foreach ($candidate in $candidates) {
    if (Test-Path -LiteralPath $candidate -PathType Leaf) {
    $asarPath = $candidate
    break
    }
    }

    if (-not $asarPath) {
    Write-Host "Error: no app.asar found under: $($item.FullName)"
    Write-Host "Expected at 'resources\app.asar' or 'app.asar'."
    Wait-Key
    exit 1
    }
    Write-Host " app.asar: $asarPath"

    Write-Host "[2/8] Building equal-length replacement string ..."
    $target = "if (layoutUpgrade()) return true;"
    $replacement = "return false;"
    $paddingLength = $target.Length - $replacement.Length

    if ($paddingLength -lt 0) {
    Write-Host "Error: replacement is longer than target."
    Wait-Key
    exit 1
    }

    $newString = $replacement + (" " * $paddingLength)
    if ($newString.Length -ne $target.Length) {
    Write-Host "Error: equal-length invariant broken."
    Wait-Key
    exit 1
    }
    Write-Host " target len: $($target.Length) / replacement len: $($newString.Length)"

    Write-Host "[3/8] Reading bytes from $asarPath ..."
    $targetBytes = [System.Text.Encoding]::ASCII.GetBytes($target)
    $newBytes = [System.Text.Encoding]::ASCII.GetBytes($newString)
    $bytes = [System.IO.File]::ReadAllBytes($asarPath)
    Write-Host " read $($bytes.Length) bytes"

    Write-Host "[4/8] Searching for target string ..."
    $index = Find-Bytes -Data $bytes -Pattern $targetBytes
    if ($index -lt 0) {
    Write-Host " Target string not found; no changes were made."
    Wait-Key
    exit 0
    }
    Write-Host " found at offset $index"

    $second = Find-Bytes -Data $bytes -Pattern $targetBytes -StartIndex ($index + 1)
    if ($second -ne -1) {
    Write-Host "Error: multiple matches found (second offset: $second); aborting."
    Wait-Key
    exit 1
    }

    Write-Host "[5/8] Creating timestamped backup ..."
    $timestamp = Get-Date -Format "yyyyMMddHHmmss"
    $backupPath = "$asarPath.$timestamp.bak"

    if (Test-Path -LiteralPath $backupPath) {
    Write-Host "Error: backup already exists: $backupPath"
    Wait-Key
    exit 1
    }

    Copy-Item -LiteralPath $asarPath -Destination $backupPath
    Write-Host " Backup created: $backupPath"

    Write-Host "[6/8] Applying equal-length replacement ..."
    $originalLength = $bytes.Length
    [System.Array]::Copy($newBytes, 0, $bytes, $index, $newBytes.Length)

    if ($bytes.Length -ne $originalLength) {
    Write-Host "Error: array length changed; aborting before write."
    Wait-Key
    exit 1
    }

    try {
    Write-Host "[7/8] Writing patched bytes back ..."
    [System.IO.File]::WriteAllBytes($asarPath, $bytes)

    Write-Host "[8/8] Verifying patched file ..."
    $verifyBytes = [System.IO.File]::ReadAllBytes($asarPath)
    if ($verifyBytes.Length -ne $originalLength) {
        throw "File length changed after write."
    }
    
    $verifySlice = $verifyBytes[$index..($index + $newBytes.Length - 1)]
    $verifyText = [System.Text.Encoding]::ASCII.GetString($verifySlice)
    if ($verifyText -ne $newString) {
        throw "Unexpected content in the patched region."
    }
    

    }
    catch {
    Write-Host "Error: patch failed: $($.Exception.Message)"
    Write-Host " Restoring backup ..."
    try {
    Copy-Item -LiteralPath $backupPath -Destination $asarPath -Force
    Write-Host " Original file restored."
    }
    catch {
    Write-Host "CRITICAL: automatic restore failed: $($
    .Exception.Message)"
    Write-Host " Restore manually from: $backupPath"
    }

    Wait-Key
    exit 1
    

    }

    Write-Host "Success: old layout locked."
    Write-Host "Backup: $backupPath"
    Wait-Key

  11. pav18 commented on Sep 14, 2026

    @pav18

    Thank you so much for your script!!!!!

  12. enngins commented on Sep 14, 2026

    @enngins

    Guys WTH!
    We really need to patch it for old (NORMAL) layout? I can't image who can work with this new dumb's interface.

  13. drobin04 commented on Sep 14, 2026

    @drobin04

    I HATE the new layout. I stayed on an old version for quite a while for the old layout, and then forgot one day and updated. HATE IT. PLEASE add a toggle to restore the old layout. The new one is AWFUL.

  14. kmanderson12 commented on Sep 14, 2026

    @kmanderson12

    The recent change to force the new layout completely breaks the mental model that the previous layout encouraged. I no longer have easy visibility over which one of my projects has active agent activity. I'm left to try to infer the activity from the thread's title, which might not always be relevant or accurate. This adds friction and confusion that is completely unnecessary.

    The old layout needs to stay as a configurable option in order to support the workflows we've grown to love as we've used this tool.

  15. Meepo268 commented on Sep 15, 2026

    @Meepo268

    写了个powersell脚本,替换opencode打包后的 asar 里面的新布局判断逻辑强制使用旧布局,执行powershell,输入asar的文件夹,通常是: C:\Users\%Username%\AppData\Local\Programs\@opencode-aidesktop\resources 注意替换路径里面的用户名,脚本会自动备份当前的asar,执行完成后重启opencode即可,支持 1.18.30 版本 注意:每次更新opencode后需要重新执行

    我编写了一个 PowerShell 脚本,用于替换 opencode 包中 asar 部分的新的布局检测逻辑,使其使用旧的布局方式。请运行 PowerShell 脚本,然后进入 ASAR 文件夹,通常该文件夹位于: C:\Users\%Username%\AppData\Local\Programs\@opencode-aidesktop\resources注意:请更换路径中的用户名。该脚本会自动备份当前的 asar 文件。完成后,只需重新启动 opencode 即可。该脚本支持版本 1.18.30。 注意:每次更新 OpenCode 后,都需要重新运行该程序。

    Image

    脚本

    由人工智能生成

    将文件复制并保存为.ps1 格式的文件,然后运行它。

    $ErrorActionPreference = "Stop"

    Wait for a key press so the console window does not close before the result is visible.

    function Wait-Key {
    Write-Host ""
    Write-Host "Press any key to exit ..."
    [void][Console]::ReadKey($true)
    }

    ---------- Step 0: locate app.asar ----------

    Write-Host "[1/8] Resolve app.asar path from the folder you provide ..."
    $inputPath = Read-Host "Enter the opencode install folder"
    $inputPath = $inputPath.Trim().Trim('"', "'")

    if ([string]::IsNullOrWhiteSpace($inputPath)) {
    Write-Host "Error: path cannot be empty."
    Wait-Key
    exit 1
    }

    if (-not (Test-Path -LiteralPath $inputPath -PathType Container)) {
    Write-Host "Error: folder not found: $inputPath"
    Wait-Key
    exit 1
    }

    只接受文件夹:优先找其下的 resources\app.asar,其次文件夹内直接存在的 app.asar

    $item = Get-Item -LiteralPath $inputPath
    $candidates = @(
    (Join-Path $item.FullName "resources\app.asar"),
    (Join-Path $item.FullName "app.asar")
    )
    $AsarPath = $null
    foreach ($candidate in $candidates) {
    if (Test-Path -LiteralPath $candidate -PathType Leaf) {
    $AsarPath = $candidate
    break
    }
    }
    if (-not $AsarPath) {
    Write-Host "Error: no app.asar found under: $($item.FullName)"
    Write-Host "Expected at 'resources\app.asar' or 'app.asar'."
    Wait-Key
    exit 1
    }
    Write-Host " app.asar: $AsarPath"

    ---------- Step 1: prepare equal-length replacement ----------

    Write-Host "[2/8] Building equal-length replacement string ..."
    $target = "if (layoutUpgrade()) return true;"
    $replacement = "return false;"
    $pad = " " * ($target.Length - $replacement.Length)
    $newString = $replacement + $pad

    Equal-length invariant must hold, otherwise the ASAR header offsets break.

    if ($newString.Length -ne $target.Length) {
    Write-Host "Error: equal-length invariant broken."
    Wait-Key
    exit 1
    }
    Write-Host " target len: $($target.Length) / replacement len: $($newString.Length)"

    ---------- Step 2: read bytes ----------

    Write-Host "[3/8] Reading bytes from $AsarPath ..."
    $targetBytes = [System.Text.Encoding]::ASCII.GetBytes($target)
    $newBytes = [System.Text.Encoding]::ASCII.GetBytes($newString)
    $bytes = [System.IO.File]::ReadAllBytes($AsarPath)
    Write-Host " read $($bytes.Length) bytes"

    Naive scan for a byte sub-sequence; good enough for a short needle.

    function Find-Bytes {
    param(
    [byte[]]$Data,
    [byte[]]$Pattern,
    [int]$StartIndex = 0
    )
    $last = $Data.Length - $Pattern.Length
    for ($i = $StartIndex; $i -le $last; $i++) {
    if ($Data[$i] -ne $Pattern[0]) { continue }
    $ok = $true
    for ($j = 1; $j -lt $Pattern.Length; $j++) {
    if ($Data[$i + $j] -ne $Pattern[$j]) {
    $ok = $false
    break
    }
    }
    if ($ok) { return $i }
    }
    return -1
    }

    ---------- Step 3: search for target ----------

    Write-Host "[4/8] Searching for target string ..."
    $index = Find-Bytes -Data $bytes -Pattern $targetBytes

    if ($index -lt 0) {
    Write-Host " Target string not found; maybe already locked."
    Wait-Key
    exit 0
    }
    Write-Host " found at offset $index"

    Ensure the match is unique so we never patch the wrong spot.

    $second = Find-Bytes -Data $bytes -Pattern $targetBytes -StartIndex ($index + 1)
    if ($second -ne -1) {
    Write-Host "Error: multiple matches found (offset $second); aborting."
    Wait-Key
    exit 1
    }

    ---------- Step 4: backup ----------

    Write-Host "[5/8] Creating backup ..."
    $bakPath = [System.IO.Path]::ChangeExtension($AsarPath, "asar.bak")
    if (-not (Test-Path -LiteralPath $bakPath)) {
    Copy-Item -LiteralPath $AsarPath -Destination $bakPath
    Write-Host " Backup created: $bakPath"
    }
    else {
    Write-Host " Backup exists, skip: $bakPath"
    }

    ---------- Step 5: patch in memory ----------

    Write-Host "[6/8] Applying equal-length replacement ..."
    $originalLength = $bytes.Length

    Overwrite the matched region with the equal-length replacement.

    [System.Array]::Copy($newBytes, 0, $bytes, $index, $newBytes.Length)

    if ($bytes.Length -ne $originalLength) {
    Write-Host "Error: array length changed; aborting before write."
    Wait-Key
    exit 1
    }

    ---------- Step 6: write back ----------

    Write-Host "[7/8] Writing patched bytes back ..."
    [System.IO.File]::WriteAllBytes($AsarPath, $bytes)

    ---------- Step 7: verify ----------

    Write-Host "[8/8] Verifying patched region ..."
    $verifyBytes = [System.IO.File]::ReadAllBytes($AsarPath)
    $verifySlice = $verifyBytes[$index..($index + $newBytes.Length - 1)]
    $verifyText = [System.Text.Encoding]::ASCII.GetString($verifySlice)
    if (-not $verifyText.StartsWith($replacement)) {
    Write-Host "Error: verification failed; unexpected content after write."
    Wait-Key
    exit 1
    }

    Write-Host "Success: old layout locked."
    Wait-Key

    太棒了,实测成功回复旧版布局!!!

  16. moesakura commented on Sep 15, 2026

    @moesakura

    I had AI generate a macOS version
    restore-old-ui.sh

  17. albgen commented on Sep 15, 2026

    @albgen

    The new UI is a significant regression for my workflow. I find it less clear, less efficient, and more difficult to navigate than the previous interface.

    The persistent left sidebar was extremely useful for quickly switching between projects, workspaces, and sessions. Removing it makes everyday work slower and more frustrating. The new layout feels especially unsuitable when working with multiple projects or sessions.

    What is most disappointing is that users are not even given a choice. Forcing everyone onto the new UI without an option to continue using the legacy interface is not acceptable, particularly while many users clearly prefer the old layout.

    Please restore the legacy UI as an optional layout, or at minimum provide a permanent setting to enable the persistent left sidebar. Users should be able to choose the interface that best fits their workflow

  18. simPod commented on Sep 15, 2026

    @simPod

    I can see shit now

    Image

    no idea what project is has working sessions and which session is which. WTH

  19. pav18 commented on Sep 16, 2026

    @pav18

    New version 1.18.31 - script doesnt work - problems in UI

  20. 0-SamboNZ-0 commented on Sep 16, 2026

    @0-SamboNZ-0

    New version 1.18.31 - script doesnt work - problems in UI

    It's working for me on that version @pav18

    What problems are you having?

  21. pav18 commented on Sep 16, 2026

    @pav18

    What problems are you having?

    Sorry, you right. My fault - also changed opencode.json with mistake

  22. ltoshea commented on Sep 16, 2026

    @ltoshea

    I just moved to opencode from other companies, not having a unified view of my sessions / projects drives me crazy. Having to ctrl+b to go to the home then click through each project individually to see my sessions is a huge pain. I run multiple sessions across multiple projects.

  23. ZenDevMaster commented on Sep 16, 2026

    @ZenDevMaster

    The old UX worked reasonably well on mobile, which is excellent for tracking your work while on the move. With the new UX, how to select a tab without closing it?

    Image
  24. ebann commented on Sep 19, 2026

    @ebann

    Old UI was definitely more convenient for me.

    The script works for now, but asking an agent to rewrite/adapt it is also possible if necessary.

  25. nasadunadu commented on Sep 24, 2026

    @nasadunadu

    Also affected — Desktop v2.0.16 on macOS (Apple silicon, zh-CN UI).

    I usually keep a lot of sessions open across several projects. The old two-pane layout (persistent session list on the left, active session on the right) was the most useful part of my workflow: one glance for an overview, one click to switch. In the new top-tab layout, that overview disappears as soon as I have more than a few sessions open; long titles get truncated, and switching always takes extra navigation.

    I also noticed Settings → Appearance → Tabs: Vertical, which is closer to the old workflow — but I'd still love a real persistent left sidebar that lists sessions (ideally grouped by project), with the current session on the right.

    Please keep a persistent left sidebar available as an option in the new layout. It doesn't have to be the default, and keeping tabs for people who prefer them is perfectly fine. Thanks!

    Environment: OpenCode Desktop v2.0.16, macOS (Apple silicon), zh-CN UI.

  26. merelyigor commented on Sep 26, 2026

    @merelyigor

    I strongly agree that the legacy layout should remain available as an officially supported option while OpenCode continues to receive the latest updates.

    I regularly switch between several projects and sessions during the day. The previous persistent project and session navigation made it easy to see what was open and move directly to the right context. In the new layout, that overview is less clear and common switches take more clicks, so I spend more time navigating the UI and less time working with the coding agent.

    I do not want to downgrade OpenCode to get that workflow back; I still want the latest fixes, provider updates, local LLM improvements, and agent features. A simple setting such as “Use legacy layout” would let users choose the layout that works best for them. If maintaining the full legacy UI is not practical, an always-visible, pinnable project/session sidebar with readable session titles would address the main usability regression.

  27. davidlopezsalvador commented on Oct 8, 2026

    @davidlopezsalvador

    This is exactly what I wanted too (the classic two-panel sidebar), so while the feature is being built I made a local workaround that restores it on Windows Desktop:

    https://github.com/davidlopezsalvador/opencode-legacy-patch

    How it works: a same-length patch inside app.asar moves oldInterfaceSunset to 2099, so resolveNewLayoutDesigns(retired, ...) stops hard-coding the new layout, and app-update.yml is neutralized so the auto-updater cannot silently wipe the patch.

    • Tested on 1.18.34 -> 1.18.35 (Electron 42, Windows); applies with the app open in ~2 minutes; one-command rollback (-Mode restore).
    • Fail-safe: the script searches the literal dynamically and aborts unless it finds exactly 1 occurrence.
    • Caveats: Windows desktop only; updates frozen until you restore -> update -> patch (script included); unofficial, use at your own risk.

    I maintain the repo, feedback welcome. This is only a stopgap until a permanent official toggle ships.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions