Skip to content
Closed
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
74 changes: 74 additions & 0 deletions packages/app/src/components/session-list-panel.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div class="flex min-w-0 flex-1 flex-col overflow-hidden rounded-[10px] bg-v2-background-bg-base shadow-[var(--v2-elevation-raised)]">
<div class="shrink-0 px-3 pt-3 pb-2">
<span class="text-13-regular text-v2-text-text-muted [font-weight:440]">
Sessions ({sessions().length})
</span>
</div>
<div class="min-h-0 flex-1 overflow-y-auto px-3 pb-3">
<div class="flex flex-col gap-px">
<For each={sessions()}>
{(session) => {
const isCurrent = session.id === props.currentSessionID
const status = session.id ? sync().data.session_status[session.id] : undefined
const working = status?.type === "busy" || status?.type === "retry"
return (
<button
type="button"
class="flex h-10 min-w-0 items-center gap-2 rounded-[6px] border-0 bg-transparent py-3 pl-3 pr-3 text-left text-v2-text-text-muted [font-weight:530] transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
classList={{
"bg-v2-overlay-simple-overlay-hover": isCurrent,
}}
onClick={() => {
if (session.id === props.currentSessionID) return
const href = params.serverKey
? sessionHref(requireServerKey(params.serverKey), session.id)
: legacySessionHref(sdk().directory, session.id)
navigate(href)
}}
>
<span class="shrink-0 w-2 text-center text-11-regular">
<Show when={working} fallback={<span class="text-v2-text-text-faint">•</span>}>
<span class="inline-block size-2 rounded-full bg-v2-icon-icon-accent" />
</Show>
</span>
<span class="min-w-0 flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-v2-text-text-base">
{sessionTitle(session.title) || session.id}
</span>
</button>
)
}}
</For>
</div>
</div>
<div class="shrink-0 border-t border-border-weaker-base px-3 py-2">
<button
type="button"
class="flex h-8 w-full cursor-default items-center gap-2 rounded-[6px] border-0 bg-transparent px-3 text-left text-v2-text-text-muted [font-weight:530] transition-[background-color] duration-[120ms] ease-in-out hover:bg-v2-overlay-simple-overlay-hover focus-visible:bg-v2-overlay-simple-overlay-hover focus-visible:outline-none"
onClick={() => navigate("/")}
>
<span class="text-v2-text-text-accent">+ New Session</span>
</button>
</div>
</div>
)
}
25 changes: 25 additions & 0 deletions packages/app/src/components/titlebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,14 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
hidden: true,
onSelect: toggleHome,
},
{
id: "sessionList.toggle",
title: "Toggle Session List",
category: language.t("command.category.view"),
keybind: "mod+shift+s",
hidden: true,
onSelect: () => layout.session.toggleList(),
},
])

command.register("tabs", () => {
Expand Down Expand Up @@ -379,6 +387,23 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl
aria-pressed={layout.route().type === "home"}
/>
</TooltipV2>
<TooltipV2
placement="bottom"
value="Session List"
class="shrink-0"
>
<IconButtonV2
type="button"
variant="ghost-muted"
size="large"
class="!w-9 shrink-0"
icon={<IconV2 name="menu" />}
state={layout.session.listOpened() ? "pressed" : undefined}
onClick={() => layout.session.toggleList()}
aria-label="Session List"
aria-pressed={layout.session.listOpened()}
/>
</TooltipV2>

<TitlebarTabStrip
tabs={tabsStore}
Expand Down
24 changes: 24 additions & 0 deletions packages/app/src/context/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
},
session: {
width: DEFAULT_SESSION_WIDTH,
listOpened: false,
},
mobileSidebar: {
opened: false,
Expand Down Expand Up @@ -750,6 +751,29 @@ export const { use: useLayout, provider: LayoutProvider } = createSimpleContext(
}
setStore("session", "width", width)
},
listOpened: createMemo(() => 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),
Expand Down
16 changes: 16 additions & 0 deletions packages/app/src/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -2254,6 +2262,14 @@ export default function Page() {
>
<Show when={!isDesktop() && !!params.id && !settings.general.newLayoutDesigns()}>{mobileTabs()}</Show>

<Show when={settings.general.newLayoutDesigns() && layout.session.listOpened()}>
<div class="min-w-0 flex" style={{ width: "280px" }}>
<Suspense>
<SessionListPanel currentSessionID={params.id} />
</Suspense>
</div>
</Show>

<div
classList={{
"@container relative shrink-0 flex flex-col min-h-0 h-full flex-1 md:flex-none transition-[width]": true,
Expand Down
114 changes: 114 additions & 0 deletions packages/tui/src/component/session-list-panel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { createMemo, For, Show } from "solid-js"
import { useTheme } from "../context/theme"
import { useSync } from "../context/sync"
import { useRoute } from "../context/route"
import { Locale } from "../util/locale"
import { Spinner } from "./spinner"
import { useTuiConfig } from "../config"
import { getScrollAcceleration } from "../util/scroll"

export function SessionListPanel(props: { sessionID: string }) {
const { theme } = useTheme()
const sync = useSync()
const { navigate } = useRoute()
const tuiConfig = useTuiConfig()
const scrollAcceleration = createMemo(() => 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 (
<box
backgroundColor={theme.backgroundPanel}
width={30}
height="100%"
paddingTop={1}
paddingBottom={1}
paddingLeft={1}
paddingRight={1}
flexShrink={0}
>
<box flexDirection="column" flexGrow={1} minHeight={0}>
<box paddingLeft={1} paddingRight={1} paddingBottom={1} flexShrink={0}>
<text fg={theme.text}>
<b>Sessions</b>
<span style={{ fg: theme.textMuted }}> ({sessionCount()})</span>
</text>
</box>
<scrollbox
flexGrow={1}
scrollAcceleration={scrollAcceleration()}
verticalScrollbarOptions={{
paddingLeft: 1,
visible: true,
trackOptions: {
backgroundColor: theme.background,
foregroundColor: theme.border,
},
}}
>
<box gap={0} paddingRight={1} paddingLeft={1}>
<For each={sessions()}>
{(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 (
<box
flexShrink={0}
paddingTop={0}
paddingBottom={0}
paddingLeft={1}
paddingRight={1}
height={2}
backgroundColor={bg()}
flexDirection="row"
onMouseUp={() => {
if (session.id !== props.sessionID) {
navigate({ type: "session", sessionID: session.id })
}
}}
>
<Show when={isWorking()} fallback={<text fg={color()} flexShrink={0} width={2}>{"\u2022 "}</text>}>
<Spinner />
</Show>
<text fg={color()} wrapMode="none" flexGrow={1}>
{" "}{Locale.truncate(session.title, 20)}
</text>
<text fg={theme.textMuted} wrapMode="none" flexShrink={0}>
{Locale.todayTimeOrDateTime(session.time.updated)}
</text>
</box>
)
}}
</For>
</box>
</scrollbox>
<box flexShrink={0} paddingTop={1}>
<box
paddingLeft={1}
paddingRight={1}
paddingTop={0}
paddingBottom={0}
onMouseUp={() => navigate({ type: "home" })}
>
<text fg={theme.accent}>+ New Session</text>
</box>
</box>
</box>
</box>
)
}
2 changes: 2 additions & 0 deletions packages/tui/src/config/keybind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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("<leader>b", "Toggle sidebar"),
session_list_panel_toggle: keybind("<leader>j", "Toggle session list panel"),
scrollbar_toggle: keybind("none", "Toggle session scrollbar"),
status_view: keybind("<leader>s", "View status"),
debug_view: keybind("none", "View debug info"),
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading