Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import { DialogPrompt } from "@tui/ui/dialog-prompt"
import { DialogTimeline } from "./dialog-timeline"
import { DialogSessionRename } from "../../component/dialog-session-rename"
import { Sidebar } from "./sidebar"
import { SessionsSidebar } from "./sessions-sidebar"
import { LANGUAGE_EXTENSIONS } from "@/lsp/language"
import parsers from "../../../../../../parsers-config.ts"
import { Clipboard } from "../../util/clipboard"
Expand Down Expand Up @@ -115,6 +116,7 @@ export function Session() {

const dimensions = useTerminalDimensions()
const [sidebar, setSidebar] = createSignal<"show" | "hide" | "auto">(kv.get("sidebar", "auto"))
const [sidebarMode, setSidebarMode] = createSignal<"context" | "sessions">(kv.get("sidebar_mode", "context"))
const [conceal, setConceal] = createSignal(true)
const [showThinking, setShowThinking] = createSignal(kv.get("thinking_visibility", true))
const [showTimestamps, setShowTimestamps] = createSignal(kv.get("timestamps", "hide") === "show")
Expand Down Expand Up @@ -408,6 +410,20 @@ export function Session() {
dialog.clear()
},
},
{
title: sidebarMode() === "context" ? "Switch to sessions sidebar" : "Switch to context sidebar",
value: "session.sidebar.mode",
keybind: "sidebar_mode_toggle",
category: "Session",
onSelect: (dialog) => {
setSidebarMode((prev) => {
const next = prev === "context" ? "sessions" : "context"
kv.set("sidebar_mode", next)
return next
})
dialog.clear()
},
},
{
title: usernameVisible() ? "Hide username" : "Show username",
value: "session.username_visible.toggle",
Expand Down Expand Up @@ -968,7 +984,12 @@ export function Session() {
<Toast />
</box>
<Show when={sidebarVisible()}>
<Sidebar sessionID={route.sessionID} />
<Show
when={sidebarMode() === "sessions"}
fallback={<Sidebar sessionID={route.sessionID} />}
>
<SessionsSidebar sessionID={route.sessionID} />
</Show>
</Show>
</box>
</context.Provider>
Expand Down
147 changes: 147 additions & 0 deletions packages/opencode/src/cli/cmd/tui/routes/session/sessions-sidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { useSync } from "@tui/context/sync"
import { createMemo, For, Show } from "solid-js"
import { useTheme } from "../../context/theme"
import { Locale } from "@/util/locale"
import { useRoute } from "@tui/context/route"
import "opentui-spinner/solid"

export function SessionsSidebar(props: { sessionID: string }) {
const sync = useSync()
const { theme } = useTheme()
const { navigate } = useRoute()

const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]

const sessions = createMemo(() => {
const today = new Date().toDateString()
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toDateString()

return sync.data.session
.filter((x) => x.parentID === undefined)
.toSorted((a, b) => b.time.updated - a.time.updated)
.map((x) => {
const date = new Date(x.time.updated)
let category = date.toDateString()

if (category === today) {
category = "Today"
} else if (category === yesterday) {
category = "Yesterday"
} else {
// Format as "Mon Dec 15"
category = date.toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric"
})
}

const status = sync.data.session_status[x.id]
const isWorking = status?.type === "busy"
const isCurrent = x.id === props.sessionID

return {
...x,
category,
isWorking,
isCurrent,
}
})
.slice(0, 50) // Limit to 50 most recent sessions
})

const groupedSessions = createMemo(() => {
const groups: Map<string, typeof sessions extends () => infer T ? T : never> = new Map()

for (const session of sessions()) {
const existing = groups.get(session.category) || []
groups.set(session.category, [...existing, session])
}

return Array.from(groups.entries())
})

return (
<box
backgroundColor={theme.backgroundPanel}
width={42}
paddingTop={1}
paddingBottom={1}
paddingLeft={2}
paddingRight={2}
>
<box flexGrow={1} gap={1}>
<box>
<text fg={theme.text}>
<b>Sessions</b>
</text>
<text fg={theme.textMuted}>{sessions().length} total</text>
</box>

<scrollbox flexGrow={1}>
<box flexShrink={0} gap={1} paddingRight={1}>
<For each={groupedSessions()}>
{([category, items]) => (
<box gap={0}>
<text fg={theme.textMuted}>
<b>{category}</b>
</text>
<For each={items}>
{(session) => (
<box
flexDirection="row"
gap={1}
backgroundColor={session.isCurrent ? theme.backgroundElement : undefined}
paddingLeft={session.isCurrent ? 1 : 0}
paddingRight={session.isCurrent ? 1 : 0}
onMouseDown={() => {
if (!session.isCurrent) {
navigate({
type: "session",
sessionID: session.id,
})
}
}}
>
<Show
when={session.isWorking}
fallback={
<text
flexShrink={0}
fg={session.isCurrent ? theme.primary : theme.textMuted}
>
{session.isCurrent ? "▶" : "•"}
</text>
}
>
<spinner
frames={spinnerFrames}
interval={80}
color={theme.primary}
/>
</Show>
<text
fg={session.isCurrent ? theme.text : theme.textMuted}
wrapMode="word"
flexGrow={1}
>
{session.title}
</text>
</box>
)}
</For>
</box>
)}
</For>
</box>
</scrollbox>
</box>

<box flexShrink={0} paddingTop={1}>
<text fg={theme.textMuted}>
<b>Ctrl+X L</b> - Full session list
</text>
</box>
</box>
)
}
1 change: 1 addition & 0 deletions packages/opencode/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ export namespace Config {
editor_open: z.string().optional().default("<leader>e").describe("Open external editor"),
theme_list: z.string().optional().default("<leader>t").describe("List available themes"),
sidebar_toggle: z.string().optional().default("<leader>b").describe("Toggle sidebar"),
sidebar_mode_toggle: z.string().optional().default("none").describe("Toggle sidebar mode between context and sessions"),
scrollbar_toggle: z.string().optional().default("none").describe("Toggle session scrollbar"),
username_toggle: z.string().optional().default("none").describe("Toggle username visibility"),
status_view: z.string().optional().default("<leader>s").describe("View status"),
Expand Down