diff --git a/packages/app/src/components/session-list-panel.tsx b/packages/app/src/components/session-list-panel.tsx
new file mode 100644
index 000000000000..845b229dd386
--- /dev/null
+++ b/packages/app/src/components/session-list-panel.tsx
@@ -0,0 +1,74 @@
+import { createMemo, For, Show } from "solid-js"
+import { useNavigate, useParams } from "@solidjs/router"
+import { useSync } from "@/context/sync"
+import { useSDK } from "@/context/sdk"
+import { sessionTitle } from "@/utils/session-title"
+import { sessionHref, legacySessionHref, requireServerKey } from "@/utils/session-route"
+
+export function SessionListPanel(props: { currentSessionID?: string }) {
+ const sync = useSync()
+ const sdk = useSDK()
+ const params = useParams()
+ const navigate = useNavigate()
+
+ const sessions = createMemo(() => {
+ return (sync().data.session ?? [])
+ .filter((s) => !s.parentID && !s.time?.archived)
+ .toSorted((a, b) => (b.time.updated ?? b.time.created) - (a.time.updated ?? a.time.created))
+ })
+
+ return (
+
store.session?.listOpened ?? false),
+ openList() {
+ if (!store.session) {
+ setStore("session", { width: DEFAULT_SESSION_WIDTH, listOpened: true })
+ return
+ }
+ setStore("session", "listOpened", true)
+ },
+ closeList() {
+ if (!store.session) {
+ setStore("session", { width: DEFAULT_SESSION_WIDTH, listOpened: false })
+ return
+ }
+ setStore("session", "listOpened", false)
+ },
+ toggleList() {
+ const value = store.session?.listOpened ?? false
+ if (!store.session) {
+ setStore("session", { width: DEFAULT_SESSION_WIDTH, listOpened: !value })
+ return
+ }
+ setStore("session", "listOpened", !value)
+ },
},
mobileSidebar: {
opened: createMemo(() => store.mobileSidebar?.opened ?? false),
diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx
index c6e3a5fcbb71..27996cda1804 100644
--- a/packages/app/src/pages/session.tsx
+++ b/packages/app/src/pages/session.tsx
@@ -83,6 +83,7 @@ import {
} from "@/pages/session/session-panel-width"
import { SessionSidePanel } from "@/pages/session/session-side-panel"
import { sessionPanelLayout } from "@/pages/session/session-panel-layout"
+import { SessionListPanel } from "@/components/session-list-panel"
import { SessionReviewEmptyChangesV2 } from "@opencode-ai/session-ui/v2/session-review-empty-changes-v2"
import { SessionReviewEmptyNoGitV2 } from "@opencode-ai/session-ui/v2/session-review-empty-no-git-v2"
import { SessionReviewV2SidebarToggle } from "@opencode-ai/session-ui/v2/session-review-v2"
@@ -1151,6 +1152,13 @@ export default function Page() {
hidden: true,
onSelect: () => command.trigger("file.open", "palette"),
},
+ {
+ id: "sessionList.toggle",
+ title: "Toggle Session List",
+ category: language.t("command.category.view"),
+ keybind: "mod+shift+s",
+ onSelect: () => layout.session.toggleList(),
+ },
])
const openReviewFile = createOpenReviewFile({
@@ -2254,6 +2262,14 @@ export default function Page() {
>
{mobileTabs()}
+
+
+
+
+
+
+
+
getScrollAcceleration(tuiConfig))
+
+ const sessions = createMemo(() => {
+ return sync.data.session
+ .filter((x) => x.parentID === undefined)
+ .toSorted((a, b) => b.time.updated - a.time.updated)
+ })
+
+ const sessionCount = createMemo(() => sessions().length)
+
+ return (
+
+
+
+
+ Sessions
+ ({sessionCount()})
+
+
+
+
+
+ {(session) => {
+ const isCurrent = session.id === props.sessionID
+ const status = () => sync.data.session_status?.[session.id]
+ const isWorking = () => status()?.type === "busy" || status()?.type === "retry"
+ const isRetry = () => status()?.type === "retry"
+ const color = () => {
+ if (isCurrent) return theme.accent
+ if (isRetry()) return theme.error
+ return theme.textMuted
+ }
+ const bg = () => (isCurrent ? theme.backgroundElement : undefined)
+
+ return (
+ {
+ if (session.id !== props.sessionID) {
+ navigate({ type: "session", sessionID: session.id })
+ }
+ }}
+ >
+ {"\u2022 "}}>
+
+
+
+ {" "}{Locale.truncate(session.title, 20)}
+
+
+ {Locale.todayTimeOrDateTime(session.time.updated)}
+
+
+ )
+ }}
+
+
+
+
+ navigate({ type: "home" })}
+ >
+ + New Session
+
+
+
+
+ )
+}
diff --git a/packages/tui/src/config/keybind.ts b/packages/tui/src/config/keybind.ts
index 5dd7e4b5aafe..a277dc888590 100644
--- a/packages/tui/src/config/keybind.ts
+++ b/packages/tui/src/config/keybind.ts
@@ -79,6 +79,7 @@ export const Definitions = {
theme_switch_mode: keybind("none", "Switch between light and dark theme mode"),
theme_mode_lock: keybind("none", "Lock or unlock theme mode"),
sidebar_toggle: keybind("b", "Toggle sidebar"),
+ session_list_panel_toggle: keybind("j", "Toggle session list panel"),
scrollbar_toggle: keybind("none", "Toggle session scrollbar"),
status_view: keybind("s", "View status"),
debug_view: keybind("none", "View debug info"),
@@ -287,6 +288,7 @@ export const CommandMap = {
theme_switch_mode: "theme.switch_mode",
theme_mode_lock: "theme.mode.lock",
sidebar_toggle: "session.sidebar.toggle",
+ session_list_panel_toggle: "session_list_panel.toggle",
scrollbar_toggle: "session.toggle.scrollbar",
status_view: "opencode.status",
debug_view: "opencode.debug",
diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx
index 6d77b0ea58fd..3c71c45a3d09 100644
--- a/packages/tui/src/routes/session/index.tsx
+++ b/packages/tui/src/routes/session/index.tsx
@@ -53,6 +53,7 @@ import { DialogConfirm } from "../../ui/dialog-confirm"
import { DialogTimeline } from "./dialog-timeline"
import { DialogForkFromTimeline } from "./dialog-fork-from-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
+import { SessionListPanel } from "../../component/session-list-panel"
import { Sidebar } from "./sidebar"
import { SubagentFooter } from "./subagent-footer.tsx"
import { filetype } from "../../util/filetype"
@@ -123,6 +124,7 @@ const sessionBindingCommands = [
"session.undo",
"session.redo",
"session.sidebar.toggle",
+ "session_list_panel.toggle",
"session.toggle.conceal",
"session.toggle.timestamps",
"session.toggle.thinking",
@@ -248,6 +250,8 @@ export function Session() {
const dimensions = useTerminalDimensions()
const [sidebar, setSidebar] = kv.signal<"auto" | "hide">("sidebar", "auto")
const [sidebarOpen, setSidebarOpen] = createSignal(false)
+ const [sessionList, setSessionList] = kv.signal<"auto" | "hide">("session_list", "auto")
+ const [sessionListOpen, setSessionListOpen] = createSignal(false)
const [conceal, setConceal] = createSignal(true)
const thinking = useThinkingMode()
const thinkingMode = thinking.mode
@@ -267,8 +271,14 @@ export function Session() {
if (sidebar() === "auto" && wide()) return true
return false
})
+ const sessionListVisible = createMemo(() => {
+ if (session()?.parentID) return false
+ if (sessionListOpen()) return true
+ if (sessionList() === "auto" && wide()) return true
+ return false
+ })
const showTimestamps = createMemo(() => timestamps() === "show")
- const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
+ const contentWidth = createMemo(() => dimensions().width - (sessionListVisible() ? 30 : 0) - (sidebarVisible() ? 42 : 0) - 4)
const providers = createMemo(() => Model.index(sync.data.provider))
const scrollAcceleration = createMemo(() => getScrollAcceleration(tuiConfig))
@@ -676,6 +686,19 @@ export function Session() {
dialog.clear()
},
},
+ {
+ title: sessionListVisible() ? "Hide session list" : "Show session list",
+ value: "session_list_panel.toggle",
+ category: "Session",
+ run: () => {
+ batch(() => {
+ const isVisible = sessionListVisible()
+ setSessionList(() => (isVisible ? "hide" : "auto"))
+ setSessionListOpen(!isVisible)
+ })
+ dialog.clear()
+ },
+ },
{
title: conceal() ? "Disable code concealment" : "Enable code concealment",
value: "session.toggle.conceal",
@@ -1163,6 +1186,26 @@ export function Session() {
}}
>
+
+
+
+
+
+
+
+
+
+
+
+