Skip to content

feat: redesign sidebar with Codex-style unified view#20242

Open
Fatih0234 wants to merge 10 commits intoanomalyco:devfrom
Fatih0234:dev
Open

feat: redesign sidebar with Codex-style unified view#20242
Fatih0234 wants to merge 10 commits intoanomalyco:devfrom
Fatih0234:dev

Conversation

@Fatih0234
Copy link
Copy Markdown

@Fatih0234 Fatih0234 commented Mar 31, 2026

Issue for this PR

Closes #20242

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Redesigns the opencode sidebar from a two-panel layout (64px rail + right panel) to a unified Codex-style sidebar with expandable folder rows.

Problem this solves:

  1. The "Add Project" button was scrolling away with many projects
  2. The two-panel layout felt disjointed with projects on left and sessions on right

Solution implemented:

  • Replaced 64px project rail with unified 256px sidebar
  • Projects now display as expandable folder rows (chevron + folder icon + name)
  • Workspaces nest as sub-folders under projects
  • Sessions nest under workspaces (or directly under single-workspace projects)
  • Added "Threads" header with toolbar (Filter, Sort, View, Add Project buttons)
  • Moved Settings and Help to bottom with text labels
  • Removed right panel entirely
  • Preserved drag-and-drop project reordering
  • Active project highlighted with background color

Files changed:

  • sidebar-shell.tsx - new unified layout with toolbar
  • sidebar-project.tsx - folder rows with expand/collapse logic
  • layout.tsx - removed right panel, simplified layout

Lines: +303, -756 (net reduction of 453 lines, cleaner codebase)

How did you verify your code works?

  • Built the app locally: cd packages/app && bun run build - successful
  • Verified all existing functionality is preserved:
    • Drag-and-drop project reordering works
    • Context menus preserved (Edit, Workspaces, Clear Notifications, Close)
    • Active project highlighting works
    • Session navigation preserved
    • All keyboard shortcuts preserved
  • Code follows existing SolidJS/TypeScript patterns
  • Uses correct Tailwind styling tokens
  • No breaking changes to API or data structures

Screenshots / recordings

Before:

[64px rail] [Right Panel: Sessions]
[U][Y][L]   Project: untitled-folder
[+]         ○ Session 1
[⚙️]        ○ Session 2
[?]         ○ Session 3

After:

[Unified Sidebar (256px)]
Threads  [f][s][v][+]
───────────────
▶ 📁 project-1
▼ 📁 project-2
  ○ Session 1
  ○ Session 2
  ▶ branch:main
▶ 📁 project-3
───────────────
⚙️ Settings
❓ Help

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

- Replace 64px project rail with unified sidebar structure
- Projects display as expandable folder rows with chevron indicators
- Add 'Threads' header with toolbar (Filter, Sort, View, Add Project buttons)
- Workspaces nest as sub-folders under projects when expanded
- Sessions nest under workspaces or directly under single-workspace projects
- Remove right panel entirely - all content in unified sidebar
- Move Settings and Help to bottom with text labels
- Preserve drag-and-drop project reordering functionality
- Add active project highlighting with background color
- Fix sidebar width at 16rem (256px) with no resize handle
@Fatih0234 Fatih0234 requested a review from adamdotdevin as a code owner March 31, 2026 10:13
Copilot AI review requested due to automatic review settings March 31, 2026 10:13
@github-actions github-actions bot added the needs:compliance This means the issue will auto-close after 2 hours. label Mar 31, 2026
@github-actions
Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Potential Related PRs Found:

  1. feat(desktop): add new sidebar for desktop #15489 - feat(desktop): add new sidebar for desktop
    feat(desktop): add new sidebar for desktop #15489

    • Related because it involves sidebar implementation for desktop, may have overlapping UI/UX patterns
  2. feat(app): add project pinning to sidebar menu #17999 - feat(app): add project pinning to sidebar menu
    feat(app): add project pinning to sidebar menu #17999

    • Related because it modifies sidebar project functionality, could conflict with the new unified layout
  3. feat(app): add dynamic sidebar sorting for active projects and sessions #11758 - feat(app): add dynamic sidebar sorting for active projects and sessions
    feat(app): add dynamic sidebar sorting for active projects and sessions #11758

    • Related because it addresses sidebar sorting for projects and sessions, which interacts with the new hierarchical structure
  4. feat(app): add sub-project groups #16934 - feat(app): add sub-project groups
    feat(app): add sub-project groups #16934

    • Related because it involves project grouping in the sidebar, which may relate to the workspace nesting in this PR

These PRs are worth reviewing to ensure compatibility and avoid conflicting changes to the sidebar architecture. However, none appear to be direct duplicates of PR #20242.

@github-actions github-actions bot removed the needs:compliance This means the issue will auto-close after 2 hours. label Mar 31, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Thanks for updating your PR! It now meets our contributing guidelines. 👍

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR redesigns the desktop sidebar UI from a split rail + right-panel layout to a single Codex-style unified sidebar that lists projects/workspaces/sessions in one scrollable view, with a fixed-width toolbar/header and bottom Settings/Help section.

Changes:

  • Replaces the old 64px project rail and sessions panel with a unified sidebar shell + header toolbar.
  • Rewrites project rendering into expandable “folder rows” with nested workspaces and sessions.
  • Removes the right panel/peek overlay/resize handle from layout.tsx and adjusts desktop nav sizing/offset behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.

File Description
packages/app/src/pages/layout/sidebar-shell.tsx New unified sidebar structure (header toolbar, scrollable project list, bottom actions).
packages/app/src/pages/layout/sidebar-project.tsx Replaces tile/hover-preview behavior with expandable project/workspace rows and inline session lists.
packages/app/src/pages/layout.tsx Removes old panel + resizing/peek UI, and updates desktop sidebar nav sizing/positioning.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 16 to 35
export const SidebarContent = (props: {
mobile?: boolean
opened: Accessor<boolean>
aimMove: (event: MouseEvent) => void
projects: Accessor<LocalProject[]>
renderProject: (project: LocalProject) => JSX.Element
handleDragStart: (event: unknown) => void
handleDragEnd: () => void
handleDragOver: (event: DragEvent) => void
openProjectLabel: JSX.Element
openProjectKeybind: Accessor<string | undefined>
onOpenProject: () => void
renderProjectOverlay: () => JSX.Element
settingsLabel: Accessor<string>
settingsKeybind: Accessor<string | undefined>
onOpenSettings: () => void
helpLabel: Accessor<string>
onOpenHelp: () => void
renderPanel: () => JSX.Element
}): JSX.Element => {
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SidebarContent's props type still requires renderPanel, but the component no longer uses it and layout.tsx no longer passes it. This will cause a TypeScript error at the call site; either remove renderPanel (and any other now-unused props like opened/aimMove if applicable) from the prop type, or reintroduce the panel rendering.

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +67
<Tooltip placement={placement()} value="Filter">
<IconButton
icon="sliders"
variant="ghost"
size="small"
aria-label="Filter"
/>
</Tooltip>
<Tooltip placement={placement()} value="Sort">
<IconButton
icon="selector"
variant="ghost"
size="small"
aria-label="Sort"
/>
</Tooltip>
<Tooltip placement={placement()} value="View">
<IconButton
icon="eye"
variant="ghost"
size="small"
aria-label="View"
/>
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Filter/Sort/View toolbar buttons are rendered as enabled IconButtons but have no click handler or disabled state, so they are focusable yet do nothing. If these actions aren't implemented yet, mark them disabled/aria-disabled (or hide them); otherwise wire them to the intended handlers/menus to avoid confusing UX and accessibility issues.

Copilot uses AI. Check for mistakes.
Comment on lines 10 to 14
import { ConstrainDragXAxis } from "@/utils/solid-dnd"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
import { Icon } from "@opencode-ai/ui/icon"
import { type LocalProject } from "@/context/layout"
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SidebarContent still imports createEffect/createMemo but no longer uses them in this file. Removing unused imports reduces dead code and avoids potential lint/CI failures.

Copilot uses AI. Check for mistakes.
workspaceLabel: (directory: string, branch?: string, projectId?: string) => string
sessionProps: Omit<SessionItemProps, "session" | "list" | "slug" | "children" | "mobile" | "dense" | "popover">
setHoverSession: (id: string | undefined) => void
}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProjectSidebarContext was changed to remove the hover/aim-related fields, but layout.tsx still constructs projectSidebarCtx with properties like hoverProject, onProjectMouseEnter, and onHoverOpenChanged. With the current exported type, that assignment will fail type-checking; either re-add the removed fields to the context type or update layout.tsx to stop passing/using them.

Suggested change
}
} & Record<string, unknown>

Copilot uses AI. Check for mistakes.
Comment on lines +116 to +149
<ContextMenu.Trigger
as="button"
type="button"
classList={{
"flex items-center gap-2 px-4 py-2 w-full text-left transition-colors": true,
"hover:bg-surface-base-hover": true,
"bg-surface-base-active": isSelected(),
}}
onClick={toggleExpand}
>
<Icon
name={isExpanded() ? "chevron-down" : "chevron-right"}
size="small"
class="text-icon-weak shrink-0"
/>
<Icon
name="folder"
size="small"
classList={{
"text-icon-base shrink-0": true,
"text-icon-interactive-base": isSelected(),
}}
/>
<span classList={{
"flex-1 truncate text-14-medium": true,
"text-text-strong": !isSelected(),
"text-text-interactive-base": isSelected(),
}}>
{displayName(props.project)}
</span>
<Show when={unseenCount() > 0}>
<div class="size-1.5 rounded-full bg-text-interactive-base shrink-0" />
</Show>
</ContextMenu.Trigger>
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The project header row lost the data-action="project-switch" / data-project attributes and no longer calls ctx.navigateToProject on click (it only toggles expansion). This breaks the existing project-switch interaction and the e2e selector that targets [data-action="project-switch"][data-project=...] (see packages/app/e2e/selectors.ts). Consider restoring those data attributes and providing a click target that navigates to the project while keeping the chevron for expand/collapse.

Copilot uses AI. Check for mistakes.
Comment on lines 1 to 8
import { createEffect, createMemo, For, Show, type Accessor, type JSX } from "solid-js"
import { createStore } from "solid-js/store"
import { base64Encode } from "@opencode-ai/util/encode"
import { Button } from "@opencode-ai/ui/button"
import { ContextMenu } from "@opencode-ai/ui/context-menu"
import { HoverCard } from "@opencode-ai/ui/hover-card"
import { Icon } from "@opencode-ai/ui/icon"
import { createSortable } from "@thisbeyond/solid-dnd"
import { useLayout, type LocalProject } from "@/context/layout"
import { useGlobalSync } from "@/context/global-sync"
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are now unused imports in this module (createEffect and useLayout are imported but not referenced after the rewrite). Cleaning these up improves maintainability and avoids potential lint/CI failures.

Copilot uses AI. Check for mistakes.
settingsKeybind={() => command.keybind("settings.open")}
onOpenSettings={openSettings}
helpLabel={() => language.t("sidebar.help")}
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SidebarContent is invoked without renderPanel, but SidebarContent's prop type in sidebar-shell.tsx still requires it. This will fail type-checking; either remove renderPanel from SidebarContent's props or pass the appropriate panel renderer here.

Suggested change
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
renderPanel={((..._args: any[]) => null) as any}

Copilot uses AI. Check for mistakes.
Comment on lines +2080 to 2082
"absolute inset-y-0 left-0 w-64": true,
"z-10": true,
}}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Desktop sidebar is now hard-coded to w-64, but the main content offset still uses side() (derived from the persisted layout.sidebar.width()) when layout.sidebar.opened() is true. If a user previously resized the sidebar, side() may not equal 16rem, causing the main area to be offset too far (empty gap) while the nav stays 256px. Consider aligning these by deriving the nav width and --main-left from the same value (or migrating/resetting persisted sidebar width now that resizing is removed).

Suggested change
"absolute inset-y-0 left-0 w-64": true,
"z-10": true,
}}
"absolute inset-y-0 left-0": true,
"z-10": true,
}}
style={{
width: layout.sidebar.opened() ? `${side()}px` : "16rem",
}}

Copilot uses AI. Check for mistakes.
Comment on lines 2132 to 2134
style={{
"--main-left": layout.sidebar.opened() ? `${side()}px` : "4rem",
"--main-left": layout.sidebar.opened() ? `${side()}px` : "16rem",
}}
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--main-left switches between ${side()}px (opened) and 16rem (closed), while the sidebar nav is fixed w-64. If side() differs from 256px (e.g., due to persisted layout.sidebar.width()), the main content left offset will no longer match the actual sidebar width. It would be safer to compute --main-left from the same fixed width (or use side() consistently for the nav width as well).

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +148
as="button"
type="button"
classList={{
"flex items-center gap-2 px-4 py-2 w-full text-left transition-colors": true,
"hover:bg-surface-base-hover": true,
"bg-surface-base-active": isSelected(),
}}
onClick={toggleExpand}
>
<Icon
name={isExpanded() ? "chevron-down" : "chevron-right"}
size="small"
class="text-icon-weak shrink-0"
/>
<Icon
name="folder"
size="small"
classList={{
"text-icon-base shrink-0": true,
"text-icon-interactive-base": isSelected(),
}}
/>
<span classList={{
"flex-1 truncate text-14-medium": true,
"text-text-strong": !isSelected(),
"text-text-interactive-base": isSelected(),
}}>
{displayName(props.project)}
</span>
<Show when={unseenCount() > 0}>
<div class="size-1.5 rounded-full bg-text-interactive-base shrink-0" />
</Show>
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new project row no longer renders a dedicated menu trigger element with data-action="project-menu" / data-project=..., but existing e2e flows look for that selector (see packages/app/e2e/selectors.ts). If the UI still needs a clickable “more options” affordance (and to keep automation stable), add an explicit menu button inside the row with those data attributes, or update the e2e actions to open the context menu via right-click.

Suggested change
as="button"
type="button"
classList={{
"flex items-center gap-2 px-4 py-2 w-full text-left transition-colors": true,
"hover:bg-surface-base-hover": true,
"bg-surface-base-active": isSelected(),
}}
onClick={toggleExpand}
>
<Icon
name={isExpanded() ? "chevron-down" : "chevron-right"}
size="small"
class="text-icon-weak shrink-0"
/>
<Icon
name="folder"
size="small"
classList={{
"text-icon-base shrink-0": true,
"text-icon-interactive-base": isSelected(),
}}
/>
<span classList={{
"flex-1 truncate text-14-medium": true,
"text-text-strong": !isSelected(),
"text-text-interactive-base": isSelected(),
}}>
{displayName(props.project)}
</span>
<Show when={unseenCount() > 0}>
<div class="size-1.5 rounded-full bg-text-interactive-base shrink-0" />
</Show>
as="div"
classList={{
"flex items-center gap-2 px-4 py-2 w-full text-left transition-colors": true,
"hover:bg-surface-base-hover": true,
"bg-surface-base-active": isSelected(),
}}
>
<button
type="button"
class="flex items-center gap-2 flex-1 text-left"
onClick={toggleExpand}
>
<Icon
name={isExpanded() ? "chevron-down" : "chevron-right"}
size="small"
class="text-icon-weak shrink-0"
/>
<Icon
name="folder"
size="small"
classList={{
"text-icon-base shrink-0": true,
"text-icon-interactive-base": isSelected(),
}}
/>
<span
classList={{
"flex-1 truncate text-14-medium": true,
"text-text-strong": !isSelected(),
"text-text-interactive-base": isSelected(),
}}
>
{displayName(props.project)}
</span>
<Show when={unseenCount() > 0}>
<div class="size-1.5 rounded-full bg-text-interactive-base shrink-0" />
</Show>
</button>
<button
type="button"
data-action="project-menu"
data-project={base64Encode(props.project.worktree)}
class="shrink-0 rounded p-1 text-icon-weak hover:text-icon-base hover:bg-surface-base-hover"
aria-label="More options"
>
<Icon name="dots-horizontal" size="small" />
</button>

Copilot uses AI. Check for mistakes.
Updates e2e tests to match the redesigned sidebar UI:
- home.spec.ts: Remove 'No projects open' text assertions, add 'Threads' header check
- selectors.ts: Add projectRowSelector for new folder row structure, deprecate old projectSwitchSelector
- sidebar-popover-actions.spec.ts: Update to work with new folder row/expand behavior
- projects-switch.spec.ts: Adapt to new project interaction model

These changes align tests with the unified Codex-style sidebar that replaces
the two-panel layout with expandable folder rows.
- titlebar-history.spec.ts: Add project expansion before session selection
- actions.ts: Update openProjectMenu to use new projectRowSelector and context menus
- selectors.ts: Already has projectRowSelector (no change needed)

Tests now properly expand project folders before trying to access session items.
The new sidebar design didn't include data-project attribute on the
project row button, causing e2e tests to fail when trying to locate
projects. This adds the missing attribute to the ContextMenu.Trigger.
The new sidebar only had context menus (right-click) which don't work
reliably in e2e tests. This adds a visible 'more' button that appears
on hover/focus to trigger the project menu, matching the old sidebar
behavior and making tests more reliable.
The menu button has opacity-0 by default and only shows on hover.
Updated openProjectMenu to hover first, then click the menu button.
Remove opacity-0 and hover classes that were hiding the menu button.
This makes the button always visible so tests can reliably click it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants