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 ( +
+
+ + Sessions ({sessions().length}) + +
+
+
+ + {(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 ( + + ) + }} + +
+
+
+ +
+
+ ) +} diff --git a/packages/app/src/components/titlebar.tsx b/packages/app/src/components/titlebar.tsx index 649a8f57773c..f9364d8dd0da 100644 --- a/packages/app/src/components/titlebar.tsx +++ b/packages/app/src/components/titlebar.tsx @@ -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", () => { @@ -379,6 +387,23 @@ export function Titlebar(props: { update?: TitlebarUpdate; debugTools?: { visibl aria-pressed={layout.route().type === "home"} /> + + } + state={layout.session.listOpened() ? "pressed" : undefined} + onClick={() => layout.session.toggleList()} + aria-label="Session List" + aria-pressed={layout.session.listOpened()} + /> + 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() { }} > + + + + + + + + + + + +