Skip to content
Open
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
10 changes: 10 additions & 0 deletions packages/tui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import { cliErrorMessage, errorFormat } from "./util/error"

const appGlobalBindingCommands = [
"session.list",
"session.toggle.global_list",
"session.new",
"session.quick_switch.1",
"session.quick_switch.2",
Expand Down Expand Up @@ -564,6 +565,15 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
dialog.replace(() => <DialogSessionList />)
},
},
{
name: "session.toggle.global_list",
title: kv.get("session_list_global_enabled", false) ? "Hide global sessions" : "Show global sessions",
category: "Session",
run: () => {
kv.set("session_list_global_enabled", !kv.get("session_list_global_enabled", false))
dialog.clear()
},
},
{
name: "session.new",
title: "New session",
Expand Down
45 changes: 33 additions & 12 deletions packages/tui/src/component/dialog-session-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { Spinner } from "./spinner"
import { errorMessage } from "../util/error"
import { DialogSessionDeleteFailed } from "./dialog-session-delete-failed"
import { useCommandShortcut } from "../keymap"
import type { GlobalSession, Session } from "@opencode-ai/sdk/v2"

type PickerSession = Session & { project?: GlobalSession["project"] }

export function DialogSessionList() {
const dialog = useDialog()
Expand All @@ -34,18 +37,26 @@ export function DialogSessionList() {
const quickSwitch9 = useCommandShortcut("session.quick_switch.9")

const [searchResults, { refetch }] = createResource(
() => ({ query: search(), filter: sync.session.query() }),
() => ({ query: search(), global: sync.session.globalListEnabled(), filter: sync.session.query() }),
async (input) => {
if (!input.query) return undefined
const result = await sdk.client.session.list({ search: input.query, limit: 30, ...input.filter })
return result.data ?? []
return sync.session.list({ search: input.query, limit: 30, roots: true })
},
)
const [browseSessions, { refetch: refetchBrowse }] = createResource(
() => ({ global: sync.session.globalListEnabled(), filter: sync.session.query() }),
async () => {
return sync.session.list({ start: Date.now() - 30 * 24 * 60 * 60 * 1000, limit: 100, roots: true })
},
)

const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined))
const sessions = createMemo(() => searchResults() ?? sync.data.session)
const sessions = createMemo<PickerSession[]>(() => {
const listed = searchResults() ?? browseSessions()
return (listed ?? []) as PickerSession[]
})

function recover(session: NonNullable<ReturnType<typeof sessions>[number]>) {
function recover(session: PickerSession) {
const workspace = project.workspace.get(session.workspaceID!)
const list = () => dialog.replace(() => <DialogSessionList />)
const warp = async (selection: WorkspaceSelection) => {
Expand Down Expand Up @@ -109,6 +120,7 @@ export function DialogSessionList() {
await project.workspace.sync()
await sync.session.refresh()
if (search()) await refetch()
else await refetchBrowse()
if (info?.workspaceID === session.workspaceID) {
route.navigate({ type: "home" })
}
Expand Down Expand Up @@ -138,8 +150,6 @@ export function DialogSessionList() {
.map((x) => x.id)
}

const [browseOrder] = createSignal<string[]>(orderByRecency(sync.data.session))

const quickSwitchHint = createMemo(() => {
const first = quickSwitch1()
const last = quickSwitch9()
Expand All @@ -160,7 +170,7 @@ export function DialogSessionList() {
)

const searchResult = searchResults()
const displayOrder = searchResult ? orderByRecency(searchResult) : browseOrder()
const displayOrder = searchResult ? orderByRecency(searchResult as PickerSession[]) : orderByRecency(sessions())

const pinned = local.session.pinned().filter((id) => sessionMap.has(id))
const pinnedSet = new Set(pinned)
Expand All @@ -174,8 +184,15 @@ export function DialogSessionList() {
? x.directory.slice(0, -x.path.length).replace(/\/$/, "")
: undefined
: x.directory
const footer =
directory && directory !== project.data.project.mainDir ? Locale.truncate(path.basename(directory), 20) : ""
const projectLabel = x.project?.name ?? (x.project ? path.basename(x.project.worktree) : undefined)
const detail = x.directory
const footer = sync.session.globalListEnabled()
? projectLabel
? Locale.truncate(projectLabel, 20)
: Locale.truncate(path.basename(directory ?? x.directory), 20)
: directory && directory !== project.data.project.mainDir
? Locale.truncate(path.basename(directory), 20)
: ""

const isDeleting = toDelete() === x.id
const status = sync.data.session_status?.[x.id]
Expand All @@ -192,6 +209,7 @@ export function DialogSessionList() {
value: x.id,
category,
footer,
details: sync.session.globalListEnabled() && detail ? [detail] : undefined,
gutter,
}
}
Expand All @@ -215,7 +233,7 @@ export function DialogSessionList() {

return (
<DialogSelect
title="Sessions"
title={sync.session.globalListEnabled() ? "Sessions (global)" : "Sessions"}
options={options()}
skipFilter={true}
current={currentSessionID()}
Expand Down Expand Up @@ -280,6 +298,7 @@ export function DialogSessionList() {
await sync.session.refresh()
}
if (search()) await refetch()
else await refetchBrowse()
setToDelete(undefined)
return
}
Expand All @@ -290,7 +309,9 @@ export function DialogSessionList() {
command: "session.rename",
title: "rename",
onTrigger: async (option) => {
dialog.replace(() => <DialogSessionRename session={option.value} />)
dialog.replace(() => (
<DialogSessionRename session={option.value} title={sessions().find((item) => item.id === option.value)?.title} />
))
},
},
]}
Expand Down
3 changes: 2 additions & 1 deletion packages/tui/src/component/dialog-session-rename.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useSDK } from "../context/sdk"

interface DialogSessionRenameProps {
session: string
title?: string
}

export function DialogSessionRename(props: DialogSessionRenameProps) {
Expand All @@ -17,7 +18,7 @@ export function DialogSessionRename(props: DialogSessionRenameProps) {
return (
<DialogPrompt
title="Rename Session"
value={session()?.title}
value={session()?.title ?? props.title}
onConfirm={(value) => {
void sdk.client.session.update({
sessionID: props.session,
Expand Down
2 changes: 2 additions & 0 deletions packages/tui/src/config/keybind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export const Definitions = {
session_copy: keybind("none", "Copy session transcript"),
session_new: keybind("<leader>n", "Create a new session"),
session_list: keybind("<leader>l", "List all sessions"),
session_toggle_global_list: keybind("none", "Toggle global sessions in the session list"),
session_timeline: keybind("<leader>g", "Show session timeline"),
session_fork: keybind("none", "Fork session from message"),
session_rename: keybind("ctrl+r", "Rename session"),
Expand Down Expand Up @@ -289,6 +290,7 @@ export const CommandMap = {
session_copy: "session.copy",
session_new: "session.new",
session_list: "session.list",
session_toggle_global_list: "session.toggle.global_list",
session_timeline: "session.timeline",
session_fork: "session.fork",
session_rename: "session.rename",
Expand Down
35 changes: 34 additions & 1 deletion packages/tui/src/context/sync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
Agent,
Provider,
Session,
GlobalSession,
Part,
Config,
Todo,
Expand Down Expand Up @@ -37,6 +38,14 @@ const emptyConsoleState: ConsoleState = {
switchableOrgCount: 0,
}

type PickerSession = Session | GlobalSession
type PickerSessionListInput = {
start?: number
search?: string
limit?: number
roots?: boolean
}

function search<T>(items: T[], target: string, key: (item: T) => string) {
let left = 0
let right = items.length - 1
Expand Down Expand Up @@ -159,10 +168,28 @@ export const {
}
}

function globalSessionListEnabled() {
return kv.get("session_list_global_enabled", false)
}

function sortSessionsByID<T extends { id: string }>(sessions: T[]) {
return sessions.toSorted((a, b) => a.id.localeCompare(b.id))
}

function listSessions() {
return sdk.client.session
.list({ start: Date.now() - 30 * 24 * 60 * 60 * 1000, ...sessionListQuery() })
.then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id)))
.then((x) => sortSessionsByID(x.data ?? []))
}

function listPickerSessions(input: PickerSessionListInput = {}): Promise<PickerSession[]> {
if (globalSessionListEnabled()) {
return sdk.client.experimental.session
// directory="" blocks SDK directory injection while staying unfiltered on the server.
.list({ directory: "", ...input })
.then((x) => sortSessionsByID(x.data ?? []))
}
return sdk.client.session.list({ ...input, ...sessionListQuery() }).then((x) => sortSessionsByID(x.data ?? []))
}

event.subscribe((event, { workspace }) => {
Expand Down Expand Up @@ -560,6 +587,12 @@ export const {
query() {
return sessionListQuery()
},
globalListEnabled() {
return globalSessionListEnabled()
},
list(input?: PickerSessionListInput) {
return listPickerSessions(input)
},
async refresh() {
const list = await listSessions()
setStore("session", reconcile(list))
Expand Down
3 changes: 2 additions & 1 deletion packages/tui/src/ui/dialog-select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
if (!category) return acc
return acc + (i > 0 ? 2 : 1)
}, 0)
return flat().reduce((acc, option) => acc + 1 + (option.details?.length ?? 0), headers)
return flat().reduce((acc, option) => acc + 1 + (option.details?.length ?? 0) + (option.details?.length ? 1 : 0), headers)
})

const dimensions = useTerminalDimensions()
Expand Down Expand Up @@ -591,6 +591,7 @@ export function DialogSelect<T>(props: DialogSelectProps<T>) {
if (index === -1) return
moveTo(index)
}}
paddingBottom={option.details?.length ? 1 : 0}
>
<box
flexDirection="row"
Expand Down
Loading
Loading