diff --git a/packages/app/src/components/settings-general.tsx b/packages/app/src/components/settings-general.tsx
index 42ee4092f68..c50386fee8c 100644
--- a/packages/app/src/components/settings-general.tsx
+++ b/packages/app/src/components/settings-general.tsx
@@ -9,7 +9,7 @@ import { useTheme, type ColorScheme } from "@opencode-ai/ui/theme"
import { showToast } from "@opencode-ai/ui/toast"
import { useLanguage } from "@/context/language"
import { usePlatform } from "@/context/platform"
-import { useSettings, monoFontFamily } from "@/context/settings"
+import { useSettings, monoFontFamily, type SidebarStyle } from "@/context/settings"
import { playSound, SOUND_OPTIONS } from "@/utils/sound"
import { Link } from "./link"
@@ -137,6 +137,11 @@ export const SettingsGeneral: Component = () => {
] as const
const fontOptionsList = [...fontOptions]
+ const sidebarStyleOptions = [
+ { value: "classic" as SidebarStyle, label: "sidebar.style.classic" as const },
+ { value: "list" as SidebarStyle, label: "sidebar.style.list" as const },
+ ]
+
const noneSound = { id: "none", label: "sound.option.none", src: undefined } as const
const soundOptions = [noneSound, ...SOUND_OPTIONS]
@@ -267,6 +272,23 @@ export const SettingsGeneral: Component = () => {
)}
+
+
+
)
diff --git a/packages/app/src/context/settings.tsx b/packages/app/src/context/settings.tsx
index b43469b5c37..0d76c9f7145 100644
--- a/packages/app/src/context/settings.tsx
+++ b/packages/app/src/context/settings.tsx
@@ -18,6 +18,8 @@ export interface SoundSettings {
errors: string
}
+export type SidebarStyle = "classic" | "list"
+
export interface Settings {
general: {
autoSave: boolean
@@ -25,6 +27,7 @@ export interface Settings {
showReasoningSummaries: boolean
shellToolPartsExpanded: boolean
editToolPartsExpanded: boolean
+ sidebarStyle: SidebarStyle
}
updates: {
startup: boolean
@@ -48,6 +51,7 @@ const defaultSettings: Settings = {
showReasoningSummaries: false,
shellToolPartsExpanded: true,
editToolPartsExpanded: false,
+ sidebarStyle: "classic" as SidebarStyle,
},
updates: {
startup: true,
@@ -147,6 +151,10 @@ export const { use: useSettings, provider: SettingsProvider } = createSimpleCont
setEditToolPartsExpanded(value: boolean) {
setStore("general", "editToolPartsExpanded", value)
},
+ sidebarStyle: withFallback(() => store.general?.sidebarStyle, defaultSettings.general.sidebarStyle),
+ setSidebarStyle(value: SidebarStyle) {
+ setStore("general", "sidebarStyle", value)
+ },
},
updates: {
startup: withFallback(() => store.updates?.startup, defaultSettings.updates.startup),
diff --git a/packages/app/src/i18n/en.ts b/packages/app/src/i18n/en.ts
index 7e95fd739df..0c30326dde3 100644
--- a/packages/app/src/i18n/en.ts
+++ b/packages/app/src/i18n/en.ts
@@ -608,6 +608,7 @@ export const dict = {
"sidebar.project.recentSessions": "Recent sessions",
"sidebar.project.viewAllSessions": "View all sessions",
"sidebar.project.clearNotifications": "Clear notifications",
+ "sidebar.threads": "Projects",
"app.name.desktop": "OpenCode Desktop",
@@ -634,6 +635,10 @@ export const dict = {
"settings.general.row.theme.description": "Customise how OpenCode is themed.",
"settings.general.row.font.title": "Font",
"settings.general.row.font.description": "Customise the mono font used in code blocks",
+ "settings.general.row.sidebarStyle.title": "Sidebar style",
+ "settings.general.row.sidebarStyle.description": "Choose between classic icon strip or list layout",
+ "sidebar.style.classic": "Classic",
+ "sidebar.style.list": "List",
"settings.general.row.reasoningSummaries.title": "Show reasoning summaries",
"settings.general.row.reasoningSummaries.description": "Display model reasoning summaries in the timeline",
"settings.general.row.shellToolPartsExpanded.title": "Expand shell tool parts",
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index cb194052d1e..fd41ed5f4e6 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -77,6 +77,7 @@ import {
import { workspaceOpenState } from "./layout/sidebar-workspace-helpers"
import { ProjectDragOverlay, SortableProject, type ProjectSidebarContext } from "./layout/sidebar-project"
import { SidebarContent } from "./layout/sidebar-shell"
+import { SidebarListContent } from "./layout/sidebar-list"
export default function Layout(props: ParentProps) {
const [store, setStore, , ready] = persisted(
@@ -176,7 +177,8 @@ export default function Layout(props: ParentProps) {
aim.reset()
})
- const sidebarHovering = createMemo(() => !layout.sidebar.opened() && state.hoverProject !== undefined)
+ const listMode = createMemo(() => settings.general.sidebarStyle() === "list")
+ const sidebarHovering = createMemo(() => !listMode() && !layout.sidebar.opened() && state.hoverProject !== undefined)
const sidebarExpanded = createMemo(() => layout.sidebar.opened() || sidebarHovering())
const setHoverProject = (value: string | undefined) => {
setState("hoverProject", value)
@@ -1510,7 +1512,13 @@ export default function Layout(props: ParentProps) {
)
createEffect(() => {
- const sidebarWidth = layout.sidebar.opened() ? layout.sidebar.width() : 48
+ const sidebarWidth = listMode()
+ ? layout.sidebar.opened()
+ ? layout.sidebar.width()
+ : 0
+ : layout.sidebar.opened()
+ ? layout.sidebar.width()
+ : 48
document.documentElement.style.setProperty("--dialog-left-margin", `${sidebarWidth}px`)
})
@@ -1969,10 +1977,19 @@ export default function Layout(props: ParentProps) {
aria-label={language.t("sidebar.nav.projectsAndSessions")}
data-component="sidebar-nav-desktop"
classList={{
- "hidden xl:block": true,
+ "hidden xl:block": !listMode() || layout.sidebar.opened(),
+ hidden: listMode() && !layout.sidebar.opened(),
"relative shrink-0": true,
}}
- style={{ width: layout.sidebar.opened() ? `${Math.max(layout.sidebar.width(), 244)}px` : "64px" }}
+ style={{
+ width: listMode()
+ ? layout.sidebar.opened()
+ ? `${Math.max(layout.sidebar.width(), 280)}px`
+ : "0px"
+ : layout.sidebar.opened()
+ ? `${Math.max(layout.sidebar.width(), 244)}px`
+ : "64px",
+ }}
ref={(el) => {
setState("nav", el)
}}
@@ -1993,48 +2010,89 @@ export default function Layout(props: ParentProps) {
}, 300)
}}
>
-
-
layout.sidebar.opened()}
- aimMove={aim.move}
- projects={() => layout.projects.list()}
- renderProject={(project) => (
-
- )}
- handleDragStart={handleDragStart}
- handleDragEnd={handleDragEnd}
- handleDragOver={handleDragOver}
- openProjectLabel={language.t("command.project.open")}
- openProjectKeybind={() => command.keybind("project.open")}
- onOpenProject={chooseProject}
- renderProjectOverlay={() => (
- layout.projects.list()} activeProject={() => store.activeProject} />
- )}
- settingsLabel={() => language.t("sidebar.settings")}
- settingsKeybind={() => command.keybind("settings.open")}
- onOpenSettings={openSettings}
- helpLabel={() => language.t("sidebar.help")}
- onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
- renderPanel={() => }
- />
-
-
- {(worktree) => (
-
-
+
+
+
layout.sidebar.opened()}
+ aimMove={aim.move}
+ projects={() => layout.projects.list()}
+ renderProject={(project) => (
+
+ )}
+ handleDragStart={handleDragStart}
+ handleDragEnd={handleDragEnd}
+ handleDragOver={handleDragOver}
+ openProjectLabel={language.t("command.project.open")}
+ openProjectKeybind={() => command.keybind("project.open")}
+ onOpenProject={chooseProject}
+ renderProjectOverlay={() => (
+ layout.projects.list()}
+ activeProject={() => store.activeProject}
+ />
+ )}
+ settingsLabel={() => language.t("sidebar.settings")}
+ settingsKeybind={() => command.keybind("settings.open")}
+ onOpenSettings={openSettings}
+ helpLabel={() => language.t("sidebar.help")}
+ onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
+ renderPanel={() => }
+ />
+
+
+ {(_worktree) => (
+
+
+
+ )}
+
+
+
+
+ >
+ }
+ >
+
+
+ layout.projects.list()}
+ sortNow={sortNow}
+ onNewSession={(directory) => navigateWithSidebarReset(`/${base64Encode(directory)}/session`)}
+ onOpenSettings={openSettings}
+ onOpenProject={chooseProject}
+ archiveSession={archiveSession}
+ openProjectLabel={() => language.t("command.project.open")}
+ settingsLabel={() => language.t("sidebar.settings")}
+ settingsKeybind={() => command.keybind("settings.open")}
+ newSessionLabel={() => language.t("command.session.new")}
+ newSessionKeybind={() => command.keybind("session.new")}
+ currentSessionId={() => params.id}
+ threadsLabel={() => language.t("sidebar.threads")}
+ helpLabel={() => language.t("sidebar.help")}
+ onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
+ />
- )}
-
-
-
+
+
@@ -2058,37 +2116,63 @@ export default function Layout(props: ParentProps) {
}}
onClick={(e) => e.stopPropagation()}
>
-
layout.sidebar.opened()}
- aimMove={aim.move}
- projects={() => layout.projects.list()}
- renderProject={(project) => (
-
- )}
- handleDragStart={handleDragStart}
- handleDragEnd={handleDragEnd}
- handleDragOver={handleDragOver}
- openProjectLabel={language.t("command.project.open")}
- openProjectKeybind={() => command.keybind("project.open")}
- onOpenProject={chooseProject}
- renderProjectOverlay={() => (
- layout.projects.list()} activeProject={() => store.activeProject} />
- )}
- settingsLabel={() => language.t("sidebar.settings")}
- settingsKeybind={() => command.keybind("settings.open")}
- onOpenSettings={openSettings}
- helpLabel={() => language.t("sidebar.help")}
- onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
- renderPanel={() => }
- />
+ layout.sidebar.opened()}
+ aimMove={aim.move}
+ projects={() => layout.projects.list()}
+ renderProject={(project) => (
+
+ )}
+ handleDragStart={handleDragStart}
+ handleDragEnd={handleDragEnd}
+ handleDragOver={handleDragOver}
+ openProjectLabel={language.t("command.project.open")}
+ openProjectKeybind={() => command.keybind("project.open")}
+ onOpenProject={chooseProject}
+ renderProjectOverlay={() => (
+ layout.projects.list()}
+ activeProject={() => store.activeProject}
+ />
+ )}
+ settingsLabel={() => language.t("sidebar.settings")}
+ settingsKeybind={() => command.keybind("settings.open")}
+ onOpenSettings={openSettings}
+ helpLabel={() => language.t("sidebar.help")}
+ onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
+ renderPanel={() => }
+ />
+ }
+ >
+ layout.projects.list()}
+ sortNow={sortNow}
+ onNewSession={(directory) => navigateWithSidebarReset(`/${base64Encode(directory)}/session`)}
+ onOpenSettings={openSettings}
+ onOpenProject={chooseProject}
+ archiveSession={archiveSession}
+ openProjectLabel={() => language.t("command.project.open")}
+ settingsLabel={() => language.t("sidebar.settings")}
+ settingsKeybind={() => command.keybind("settings.open")}
+ newSessionLabel={() => language.t("command.session.new")}
+ newSessionKeybind={() => command.keybind("session.new")}
+ currentSessionId={() => params.id}
+ threadsLabel={() => language.t("sidebar.threads")}
+ helpLabel={() => language.t("sidebar.help")}
+ onOpenHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")}
+ />
+
}>
diff --git a/packages/app/src/pages/layout/sidebar-items.tsx b/packages/app/src/pages/layout/sidebar-items.tsx
index eecfd17b5fd..e93fccb0890 100644
--- a/packages/app/src/pages/layout/sidebar-items.tsx
+++ b/packages/app/src/pages/layout/sidebar-items.tsx
@@ -11,14 +11,14 @@ import { HoverCard } from "@opencode-ai/ui/hover-card"
import { Icon } from "@opencode-ai/ui/icon"
import { IconButton } from "@opencode-ai/ui/icon-button"
import { MessageNav } from "@opencode-ai/ui/message-nav"
-import { Spinner } from "@opencode-ai/ui/spinner"
+
import { Tooltip } from "@opencode-ai/ui/tooltip"
import { getFilename } from "@opencode-ai/util/path"
import { type Message, type Session, type TextPart, type UserMessage } from "@opencode-ai/sdk/v2/client"
-import { For, Match, Show, Switch, createMemo, onCleanup, type Accessor, type JSX } from "solid-js"
+import { For, Show, createMemo, onCleanup, type Accessor, type JSX } from "solid-js"
import { agentColor } from "@/utils/agent"
import { hasProjectPermissions } from "./helpers"
-import { sessionPermissionRequest } from "../session/composer/session-request-tree"
+import { SessionStatusIndicator } from "./sidebar-session-status"
const OPENCODE_PROJECT_ID = "4b0ea68d7af9a6031a7ffda7ad66e0cb83315750"
@@ -89,10 +89,6 @@ const SessionRow = (props: {
mobile?: boolean
dense?: boolean
tint: Accessor
- isWorking: Accessor
- hasPermissions: Accessor
- hasError: Accessor
- unseenCount: Accessor
setHoverSession: (id: string | undefined) => void
clearHoverProjectSoon: () => void
sidebarOpened: Accessor
@@ -119,20 +115,7 @@ const SessionRow = (props: {
class="shrink-0 size-6 flex items-center justify-center"
style={{ color: props.tint() ?? "var(--icon-interactive-base)" }}
>
- }>
-
-
-
-
-
-
-
-
-
- 0}>
-
-
-
+
{props.session.title}
@@ -198,22 +181,8 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
const navigate = useNavigate()
const layout = useLayout()
const language = useLanguage()
- const notification = useNotification()
- const permission = usePermission()
const globalSync = useGlobalSync()
- const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
- const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
const [sessionStore] = globalSync.child(props.session.directory)
- const hasPermissions = createMemo(() => {
- return !!sessionPermissionRequest(sessionStore.session, sessionStore.permission, props.session.id, (item) => {
- return !permission.autoResponds(item, props.session.directory)
- })
- })
- const isWorking = createMemo(() => {
- if (hasPermissions()) return false
- const status = sessionStore.session_status[props.session.id]
- return status?.type === "busy" || status?.type === "retry"
- })
const tint = createMemo(() => {
const messages = sessionStore.message[props.session.id]
@@ -267,10 +236,6 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
mobile={props.mobile}
dense={props.dense}
tint={tint}
- isWorking={isWorking}
- hasPermissions={hasPermissions}
- hasError={hasError}
- unseenCount={unseenCount}
setHoverSession={props.setHoverSession}
clearHoverProjectSoon={props.clearHoverProjectSoon}
sidebarOpened={layout.sidebar.opened}
diff --git a/packages/app/src/pages/layout/sidebar-list.tsx b/packages/app/src/pages/layout/sidebar-list.tsx
new file mode 100644
index 00000000000..a8d811de2e3
--- /dev/null
+++ b/packages/app/src/pages/layout/sidebar-list.tsx
@@ -0,0 +1,242 @@
+import { createMemo, createSignal, For, Show, type Accessor, type JSX } from "solid-js"
+import { A } from "@solidjs/router"
+import { base64Encode } from "@opencode-ai/util/encode"
+import { Icon } from "@opencode-ai/ui/icon"
+import { IconButton } from "@opencode-ai/ui/icon-button"
+import { DiffChanges } from "@opencode-ai/ui/diff-changes"
+import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip"
+import { type Session } from "@opencode-ai/sdk/v2/client"
+import { type LocalProject } from "@/context/layout"
+import { useGlobalSync } from "@/context/global-sync"
+import { useLanguage } from "@/context/language"
+import { sortedRootSessions, displayName } from "./helpers"
+import { SessionStatusIndicator } from "./sidebar-session-status"
+
+function relativeTime(timestamp: number, now: number, language: ReturnType) {
+ const diff = now - timestamp
+ const minutes = Math.floor(diff / 60000)
+ if (minutes < 1) return language.t("common.time.justNow")
+ if (minutes < 60) return language.t("common.time.minutesAgo.short", { count: minutes })
+ const hours = Math.floor(minutes / 60)
+ if (hours < 24) return language.t("common.time.hoursAgo.short", { count: hours })
+ const days = Math.floor(hours / 24)
+ return language.t("common.time.daysAgo.short", { count: days })
+}
+
+type ProjectGroup = {
+ project: LocalProject
+ sessions: Session[]
+ slug: string
+}
+
+export const SidebarListContent = (props: {
+ projects: Accessor
+ sortNow: Accessor
+ onNewSession: (directory: string) => void
+ onOpenSettings: () => void
+ onOpenProject: () => void
+ archiveSession: (session: Session) => Promise
+ openProjectLabel: Accessor
+ settingsLabel: Accessor
+ settingsKeybind: Accessor
+ newSessionLabel: Accessor
+ newSessionKeybind: Accessor
+ currentSessionId: Accessor
+ threadsLabel: Accessor
+ helpLabel: Accessor
+ onOpenHelp: () => void
+}): JSX.Element => {
+ const globalSync = useGlobalSync()
+ const language = useLanguage()
+
+ const groups = createMemo((): ProjectGroup[] =>
+ props.projects().map((project) => {
+ const [store] = globalSync.child(project.worktree, { bootstrap: false })
+ const sessions = sortedRootSessions(store, props.sortNow())
+ return { project, sessions, slug: base64Encode(project.worktree) }
+ }),
+ )
+
+ return (
+
+ {/* Projects header */}
+
+ {props.threadsLabel()}
+
+
+
+
+
+ {/* Session list grouped by project */}
+
+
+ {/* Bottom - Settings */}
+
+
+
+
+
+
+ )
+}
+
+const ProjectGroupItem = (props: {
+ group: ProjectGroup
+ onNewSession: (directory: string) => void
+ archiveSession: (session: Session) => Promise
+ newSessionLabel: Accessor
+ currentSessionId: Accessor
+ sortNow: Accessor
+ language: ReturnType
+}): JSX.Element => {
+ const [collapsed, setCollapsed] = createSignal(false)
+
+ return (
+
+
+
+
+ {
+ event.preventDefault()
+ event.stopPropagation()
+ props.onNewSession(props.group.project.worktree)
+ }}
+ />
+
+
+
+
+
+ {(session) => (
+
+ )}
+
+
+
+
+ )
+}
+
+const SessionRow = (props: {
+ session: Session
+ slug: string
+ currentSessionId: Accessor
+ sortNow: Accessor
+ archiveSession: (session: Session) => Promise
+ language: ReturnType
+}): JSX.Element => {
+ const [confirming, setConfirming] = createSignal(false)
+ const active = createMemo(() => props.currentSessionId() === props.session.id)
+ const time = createMemo(() => {
+ const updated = props.session.time.updated ?? props.session.time.created
+ return relativeTime(updated, props.sortNow(), props.language)
+ })
+
+ return (
+ setConfirming(false)}
+ >
+
+
+
+ {props.session.title}
+
+ {/* Diff changes + time: visible by default, hidden on hover */}
+
+ {time()}}>
+ {(summary) => }
+
+
+ {/* Archive button: hidden by default, visible on hover */}
+
+
+ {
+ event.preventDefault()
+ event.stopPropagation()
+ setConfirming(true)
+ }}
+ />
+
+ }
+ >
+
+
+
+
+
+ )
+}
diff --git a/packages/app/src/pages/layout/sidebar-session-status.tsx b/packages/app/src/pages/layout/sidebar-session-status.tsx
new file mode 100644
index 00000000000..e65448300af
--- /dev/null
+++ b/packages/app/src/pages/layout/sidebar-session-status.tsx
@@ -0,0 +1,45 @@
+import { createMemo, Match, Switch, type JSX } from "solid-js"
+import { Icon } from "@opencode-ai/ui/icon"
+import { Spinner } from "@opencode-ai/ui/spinner"
+import { type Session } from "@opencode-ai/sdk/v2/client"
+import { useGlobalSync } from "@/context/global-sync"
+import { useNotification } from "@/context/notification"
+import { usePermission } from "@/context/permission"
+import { sessionPermissionRequest } from "../session/composer/session-request-tree"
+
+export const SessionStatusIndicator = (props: { session: Session }): JSX.Element => {
+ const notification = useNotification()
+ const permission = usePermission()
+ const globalSync = useGlobalSync()
+ const [store] = globalSync.child(props.session.directory)
+
+ const unseenCount = createMemo(() => notification.session.unseenCount(props.session.id))
+ const hasError = createMemo(() => notification.session.unseenHasError(props.session.id))
+ const hasPermissions = createMemo(() => {
+ return !!sessionPermissionRequest(store.session, store.permission, props.session.id, (item) => {
+ return !permission.autoResponds(item, props.session.directory)
+ })
+ })
+ const isWorking = createMemo(() => {
+ if (hasPermissions()) return false
+ const status = store.session_status[props.session.id]
+ return status?.type === "busy" || status?.type === "retry"
+ })
+
+ return (
+ }>
+
+
+
+
+
+
+
+
+
+ 0}>
+
+
+
+ )
+}