From e50d9f461adead2cf277b7e3d70a94fe1168c394 Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Fri, 9 Jan 2026 16:02:53 +0000 Subject: [PATCH] feat(ui): thread sessions in sidebar list Show sessions as parent/child threads with expand/collapse and improved agent row styling. Keep a 5-session cache to avoid refetching messages when switching between recently visited sessions. --- .../components/instance/instance-shell2.tsx | 62 +++-- packages/ui/src/components/session-list.tsx | 181 ++++++++++----- packages/ui/src/stores/session-state.ts | 56 +++++ packages/ui/src/stores/sessions.ts | 2 + packages/ui/src/styles/panels.css | 211 ------------------ .../ui/src/styles/panels/session-layout.css | 74 +++++- packages/ui/src/styles/tokens.css | 4 + 7 files changed, 306 insertions(+), 284 deletions(-) diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx index 2f4e1929..421376cd 100644 --- a/packages/ui/src/components/instance/instance-shell2.tsx +++ b/packages/ui/src/components/instance/instance-shell2.tsx @@ -29,11 +29,15 @@ import PushPinOutlinedIcon from "@suid/icons-material/PushPinOutlined" import type { Instance } from "../../types/instance" import type { Command } from "../../lib/commands" import type { BackgroundProcess } from "../../../../server/src/api-types" +import type { Session } from "../../types/session" import { activeParentSessionId, activeSessionId as activeSessionMap, getSessionFamily, getSessionInfo, + getSessionThreads, + sessions, + setActiveParentSession, setActiveSession, } from "../../stores/sessions" import { keyboardRegistry, type KeyboardShortcut } from "../../lib/keyboard-registry" @@ -87,7 +91,7 @@ const MAX_SESSION_SIDEBAR_WIDTH = 360 const RIGHT_DRAWER_WIDTH = 260 const MIN_RIGHT_DRAWER_WIDTH = 200 const MAX_RIGHT_DRAWER_WIDTH = 380 -const SESSION_CACHE_LIMIT = 2 +const SESSION_CACHE_LIMIT = 5 const APP_BAR_HEIGHT = 56 const LEFT_DRAWER_STORAGE_KEY = "opencode-session-sidebar-width-v8" const RIGHT_DRAWER_STORAGE_KEY = "opencode-session-right-drawer-width-v1" @@ -268,6 +272,12 @@ const InstanceShell2: Component = (props) => { requestAnimationFrame(() => measureDrawerHost()) }) + const allInstanceSessions = createMemo>(() => { + return sessions().get(props.instance.id) ?? new Map() + }) + + const sessionThreads = createMemo(() => getSessionThreads(props.instance.id)) + const activeSessions = createMemo(() => { const parentId = activeParentSessionId().get(props.instance.id) if (!parentId) return new Map[number]>() @@ -477,7 +487,26 @@ const InstanceShell2: Component = (props) => { }) const handleSessionSelect = (sessionId: string) => { - setActiveSession(props.instance.id, sessionId) + if (sessionId === "info") { + setActiveSession(props.instance.id, sessionId) + return + } + + const session = allInstanceSessions().get(sessionId) + if (!session) return + + if (session.parentId === null) { + setActiveParentSession(props.instance.id, sessionId) + return + } + + const parentId = session.parentId + if (!parentId) return + + batch(() => { + setActiveParentSession(props.instance.id, parentId) + setActiveSession(props.instance.id, sessionId) + }) } @@ -522,23 +551,27 @@ const InstanceShell2: Component = (props) => { }) createEffect(() => { - const sessionsMap = activeSessions() - const parentId = parentSessionIdForInstance() + const instanceSessions = allInstanceSessions() const activeId = activeSessionIdForInstance() + setCachedSessionIds((current) => { - const next: string[] = [] - const append = (id: string | null) => { + const next = current.filter((id) => id !== "info" && instanceSessions.has(id)) + + const touch = (id: string | null) => { if (!id || id === "info") return - if (!sessionsMap.has(id)) return - if (next.includes(id)) return - next.push(id) + if (!instanceSessions.has(id)) return + + const index = next.indexOf(id) + if (index !== -1) { + next.splice(index, 1) + } + next.unshift(id) } - append(parentId) - append(activeId) + touch(activeId) + + const trimmed = next.length > SESSION_CACHE_LIMIT ? next.slice(0, SESSION_CACHE_LIMIT) : next - const limit = parentId ? SESSION_CACHE_LIMIT + 1 : SESSION_CACHE_LIMIT - const trimmed = next.length > limit ? next.slice(0, limit) : next const trimmedSet = new Set(trimmed) const removed = current.filter((id) => !trimmedSet.has(id)) if (removed.length) { @@ -832,7 +865,8 @@ const InstanceShell2: Component = (props) => {
{ diff --git a/packages/ui/src/components/session-list.tsx b/packages/ui/src/components/session-list.tsx index e9b25ef6..ba1ba8fb 100644 --- a/packages/ui/src/components/session-list.tsx +++ b/packages/ui/src/components/session-list.tsx @@ -1,7 +1,8 @@ -import { Component, For, Show, createSignal, createMemo, JSX } from "solid-js" +import { Component, For, Show, createSignal, createMemo, createEffect, JSX } from "solid-js" import type { Session, SessionStatus } from "../types/session" +import type { SessionThread } from "../stores/session-state" import { getSessionStatus } from "../stores/session-status" -import { MessageSquare, Info, X, Copy, Trash2, Pencil, ShieldAlert } from "lucide-solid" +import { Bot, User, Info, X, Copy, Trash2, Pencil, ShieldAlert, ChevronDown } from "lucide-solid" import KeyboardHint from "./keyboard-hint" import Kbd from "./kbd" import SessionRenameDialog from "./session-rename-dialog" @@ -18,6 +19,7 @@ const log = getLogger("session") interface SessionListProps { instanceId: string sessions: Map + threads: SessionThread[] activeSessionId: string | null onSelect: (sessionId: string) => void onClose: (sessionId: string) => void @@ -67,7 +69,38 @@ const SessionList: Component = (props) => { return deleting ? deleting.has(sessionId) : false } + const [expandedParents, setExpandedParents] = createSignal>(new Set()) + + const toggleParentExpanded = (parentId: string) => { + setExpandedParents((prev) => { + const next = new Set(prev) + if (next.has(parentId)) { + next.delete(parentId) + } else { + next.add(parentId) + } + return next + }) + } + + const ensureParentExpanded = (parentId: string) => { + setExpandedParents((prev) => { + if (prev.has(parentId)) return prev + const next = new Set(prev) + next.add(parentId) + return next + }) + } + const selectSession = (sessionId: string) => { + if (sessionId !== "info") { + const session = props.sessions.get(sessionId) + const parentId = session?.parentId ?? session?.id + if (parentId) { + ensureParentExpanded(parentId) + } + } + props.onSelect(sessionId) } @@ -127,7 +160,15 @@ const SessionList: Component = (props) => { } - const SessionRow: Component<{ sessionId: string; canClose?: boolean }> = (rowProps) => { + const SessionRow: Component<{ + sessionId: string + canClose?: boolean + isChild?: boolean + isLastChild?: boolean + hasChildren?: boolean + expanded?: boolean + onToggleExpand?: () => void + }> = (rowProps) => { const session = () => props.sessions.get(rowProps.sessionId) if (!session()) { return <> @@ -144,16 +185,21 @@ const SessionList: Component = (props) => {