feat(tui): pin, quick-switch, and cycle recent sessions (#26858)

This commit is contained in:
Shoubhit Dash
2026-05-11 22:46:27 +05:30
committed by GitHub
parent 9835fb0083
commit 786bcb86f9
6 changed files with 373 additions and 38 deletions
@@ -7,6 +7,7 @@ import { Locale } from "@/util/locale"
import { useProject } from "@tui/context/project"
import { useTheme } from "../context/theme"
import { useSDK } from "../context/sdk"
import { useLocal } from "../context/local"
import { Flag } from "@opencode-ai/core/flag/flag"
import { DialogSessionRename } from "./dialog-session-rename"
import { createDebouncedSignal } from "../util/signal"
@@ -25,6 +26,7 @@ export function DialogSessionList() {
const project = useProject()
const { theme } = useTheme()
const sdk = useSDK()
const local = useLocal()
const toast = useToast()
const [toDelete, setToDelete] = createSignal<string>()
const [search, setSearch] = createDebouncedSignal("", 150)
@@ -128,7 +130,10 @@ export function DialogSessionList() {
const [browseOrder] = createSignal<string[]>(orderByRecency(sync.data.session))
const RECENT_LIMIT = 5
const options = createMemo(() => {
const enabled = Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
const today = new Date().toDateString()
const sessionMap = new Map(
sessions()
@@ -139,46 +144,74 @@ export function DialogSessionList() {
const searchResult = searchResults()
const displayOrder = searchResult ? orderByRecency(searchResult) : browseOrder()
return displayOrder
.map((id) => sessionMap.get(id))
.filter((x) => x !== undefined)
.map((x) => {
const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined
const dismissed = enabled ? new Set(local.session.dismissedRecent()) : new Set<string>()
const pinned = enabled ? local.session.pinned().filter((id) => sessionMap.has(id)) : []
const pinnedSet = new Set(pinned)
const slotByID = enabled
? new Map<string, number>(local.session.slots().map((id, i) => [id, i + 1]))
: new Map<string, number>()
let footer: JSX.Element | string = ""
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
if (x.workspaceID) {
footer = workspace ? (
<WorkspaceLabel
type={workspace.type}
name={workspace.name}
status={project.workspace.status(x.workspaceID) ?? "error"}
/>
) : (
<WorkspaceLabel type="unknown" name={x.workspaceID} status="error" />
)
}
} else {
footer = Locale.time(x.time.updated)
}
const recent = enabled
? displayOrder.filter((id) => !pinnedSet.has(id) && !dismissed.has(id)).slice(0, RECENT_LIMIT)
: []
const recentSet = new Set(recent)
const date = new Date(x.time.updated)
let category = date.toDateString()
if (category === today) {
category = "Today"
}
const isDeleting = toDelete() === x.id
const status = sync.data.session_status?.[x.id]
const isWorking = status?.type === "busy" || status?.type === "retry"
return {
title: isDeleting ? `Press ${deleteHint()} again to confirm` : x.title,
bg: isDeleting ? theme.error : undefined,
value: x.id,
category,
footer,
gutter: isWorking ? () => <Spinner /> : undefined,
function buildOption(id: string, category: string) {
const x = sessionMap.get(id)
if (!x) return undefined
const workspace = x.workspaceID ? project.workspace.get(x.workspaceID) : undefined
let footer: JSX.Element | string = ""
if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
if (x.workspaceID) {
footer = workspace ? (
<WorkspaceLabel
type={workspace.type}
name={workspace.name}
status={project.workspace.status(x.workspaceID) ?? "error"}
/>
) : (
<WorkspaceLabel type="unknown" name={x.workspaceID} status="error" />
)
}
} else {
footer = Locale.time(x.time.updated)
}
const isDeleting = toDelete() === x.id
const status = sync.data.session_status?.[x.id]
const isWorking = status?.type === "busy" || status?.type === "retry"
const slot = slotByID.get(x.id)
const gutter = isWorking
? () => <Spinner />
: slot !== undefined
? () => <text fg={theme.accent}>{slot}</text>
: undefined
return {
title: isDeleting ? `Press ${deleteHint()} again to confirm` : x.title,
bg: isDeleting ? theme.error : undefined,
value: x.id,
category,
footer,
gutter,
}
}
const remaining = displayOrder
.filter((id) => !pinnedSet.has(id) && !recentSet.has(id))
.map((id) => {
const x = sessionMap.get(id)
if (!x) return undefined
const label = new Date(x.time.updated).toDateString()
return buildOption(id, label === today ? "Today" : label)
})
.filter((x) => x !== undefined)
return [
...pinned.map((id) => buildOption(id, "Pinned")).filter((x) => x !== undefined),
...recent.map((id) => buildOption(id, "Recent")).filter((x) => x !== undefined),
...remaining,
]
})
onMount(() => {
@@ -203,6 +236,32 @@ export function DialogSessionList() {
dialog.clear()
}}
actions={[
...(Flag.OPENCODE_EXPERIMENTAL_SESSION_SWITCHING
? [
{
command: "session.pin.toggle",
title: "pin/unpin",
onTrigger: (option: { value: string }) => {
local.session.togglePin(option.value)
},
},
{
command: "session.toggle.recent",
title: "toggle recent",
onTrigger: (option: { value: string }) => {
if (local.session.isPinned(option.value)) {
toast.show({
variant: "info",
message: "Unpin the session first to toggle it in Recent",
duration: 3000,
})
return
}
local.session.toggleRecent(option.value)
},
},
]
: []),
{
command: "session.delete",
title: "delete",