diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 4b177e292cf..536d116edb0 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -10,6 +10,7 @@ import { DialogProvider as DialogProviderList } from "@tui/component/dialog-prov import { SDKProvider, useSDK } from "@tui/context/sdk" import { SyncProvider, useSync } from "@tui/context/sync" import { LocalProvider, useLocal } from "@tui/context/local" +import { ProjectProvider, useProjectState } from "@tui/context/directory" import { DialogModel, useConnected } from "@tui/component/dialog-model" import { DialogMcp } from "@tui/component/dialog-mcp" import { DialogStatus } from "@tui/component/dialog-status" @@ -106,6 +107,7 @@ export function tui(input: { fetch?: typeof fetch events?: EventSource onExit?: () => Promise + switchProject?: (project: string) => Promise }) { // promise to prevent immediate exit return new Promise(async (resolve) => { @@ -126,34 +128,9 @@ export function tui(input: { - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -180,6 +157,36 @@ export function tui(input: { }) } +function ProjectShell(props: { url: string; fetch?: typeof fetch; events?: EventSource; mode: "dark" | "light" }) { + const projectState = useProjectState() + + return ( + + + + + + + + + + + + + + + + + + + + + + + + ) +} + function App() { const route = useRoute() const dimensions = useTerminalDimensions() diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 85c174c1dcb..a7c9cc477a0 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -1,7 +1,9 @@ import { useDialog } from "@tui/ui/dialog" -import { DialogSelect } from "@tui/ui/dialog-select" +import { DialogSelect, type DialogSelectRef } from "@tui/ui/dialog-select" +import { DialogConfirm } from "@tui/ui/dialog-confirm" import { useRoute } from "@tui/context/route" import { useSync } from "@tui/context/sync" +import { useKeyboard } from "@opentui/solid" import { createMemo, createSignal, createResource, onMount, Show } from "solid-js" import { Locale } from "@/util/locale" import { useKeybind } from "../context/keybind" @@ -10,9 +12,24 @@ import { useSDK } from "../context/sdk" import { DialogSessionRename } from "./dialog-session-rename" import { useKV } from "../context/kv" import { createDebouncedSignal } from "../util/signal" +import { useProjectState } from "../context/directory" +import { Global } from "@/global" +import path from "path" import "opentui-spinner/solid" -export function DialogSessionList() { +type SessionOptionValue = { + id: string + directory: string +} + +export function DialogSessionList( + props: { + project?: string + initialSearch?: string + initialSelection?: SessionOptionValue + initialScrollTop?: number + } = {}, +) { const dialog = useDialog() const route = useRoute() const sync = useSync() @@ -20,21 +37,80 @@ export function DialogSessionList() { const { theme } = useTheme() const sdk = useSDK() const kv = useKV() + const projectState = useProjectState() + const clientCache = new Map>() + const sessionCache = new Map() + let dialogRef: DialogSelectRef | undefined const [toDelete, setToDelete] = createSignal() - const [search, setSearch] = createDebouncedSignal("", 150) + const [search, setSearch] = createDebouncedSignal(props.initialSearch ?? "", 150) + + const currentProject = createMemo(() => sync.data.path.directory || projectState.current || process.cwd()) + const activeProject = createMemo(() => props.project ?? currentProject()) + const isCurrentProject = createMemo(() => activeProject() === currentProject()) + + const clientFor = (project: string) => { + const current = currentProject() + if (project === current) return sdk.client + const cached = clientCache.get(project) + if (cached) return cached + const client = sdk.createClient(project) + clientCache.set(project, client) + return client + } - const [searchResults] = createResource(search, async (query) => { - if (!query) return undefined - const result = await sdk.client.session.list({ search: query, limit: 30 }) - return result.data ?? [] + const [searchResults] = createResource( + () => { + const query = search() + if (!query) return undefined + return { query, directory: activeProject() } + }, + async (input) => { + if (!input) return undefined + const client = clientFor(input.directory) + const result = await client.session.list({ search: input.query, limit: 30, directory: input.directory }) + return result.data ?? [] + }, + ) + + const [projectSessions] = createResource(activeProject, async (project) => { + const current = currentProject() + if (!project) return undefined + if (project === current) return undefined + const cached = sessionCache.get(project) + if (cached) return cached + const client = clientFor(project) + const result = await client.session.list({ limit: 200, directory: project }) + const data = result.data ?? [] + sessionCache.set(project, data) + return data }) + const projectKeybind = createMemo(() => keybind.all.session_project?.[0]) + const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) - const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] + const sessions = createMemo(() => { + const searched = searchResults() + if (searched !== undefined) return searched + if (!isCurrentProject()) { + const scoped = projectSessions() + if (scoped !== undefined) return scoped + return sessionCache.get(activeProject()) ?? [] + } + const current = currentProject() + return sync.data.session.filter((item) => item.directory === current) + }) - const sessions = createMemo(() => searchResults() ?? sync.data.session) + const currentSession = createMemo(() => { + const sessionID = currentSessionID() + if (!sessionID) return undefined + const session = sessions().find((item) => item.id === sessionID) + if (!session) return undefined + return { id: session.id, directory: session.directory } + }) + + const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] const options = createMemo(() => { const today = new Date().toDateString() @@ -50,10 +126,11 @@ export function DialogSessionList() { const isDeleting = toDelete() === x.id const status = sync.data.session_status?.[x.id] const isWorking = status?.type === "busy" + const value: SessionOptionValue = { id: x.id, directory: x.directory } return { title: isDeleting ? `Press ${keybind.print("session_delete")} again to confirm` : x.title, bg: isDeleting ? theme.error : undefined, - value: x.id, + value, category, footer: Locale.time(x.time.updated), gutter: isWorking ? ( @@ -65,47 +142,206 @@ export function DialogSessionList() { }) }) + useKeyboard((evt) => { + if (options().length > 0) return + if (!keybind.match("session_project", evt)) return + evt.preventDefault() + evt.stopPropagation() + showProjectSelect() + }) + + const showProjectSelect = () => { + const current = activeProject() + dialog.replace(() => ( + { + dialog.replace(() => ) + }} + onCancel={() => { + dialog.replace(() => ) + }} + /> + )) + } + + const openSession = async (option: { value: SessionOptionValue }) => { + const sessionID = option.value.id + const project = option.value.directory + const current = currentProject() + if (project !== current) { + const display = project.replace(Global.Path.home, "~") + const restore = { + project: activeProject(), + search: dialogRef?.filter ?? search(), + selection: option.value, + scrollTop: dialogRef?.scrollTop ?? 0, + } + const confirmed = await DialogConfirm.show(dialog, "Switch project", `Switch to ${display} to open this session?`) + if (!confirmed) { + setTimeout(() => { + dialog.replace(() => ( + + )) + }, 1) + return + } + await projectState.switchTo(project) + await sync.bootstrap() + route.navigate({ + type: "session", + sessionID, + }) + dialog.clear() + return + } + route.navigate({ + type: "session", + sessionID, + }) + dialog.clear() + } + onMount(() => { dialog.setSize("large") }) return ( (dialogRef = ref)} title="Sessions" options={options()} skipFilter={true} - current={currentSessionID()} + current={currentSession()} + selected={props.initialSelection} + initialFilter={props.initialSearch} + initialScrollTop={props.initialScrollTop} onFilter={setSearch} onMove={() => { setToDelete(undefined) }} - onSelect={(option) => { - route.navigate({ - type: "session", - sessionID: option.value, - }) - dialog.clear() - }} + onSelect={openSession} keybind={[ { keybind: keybind.all.session_delete?.[0], title: "delete", + disabled: !isCurrentProject(), onTrigger: async (option) => { - if (toDelete() === option.value) { + if (toDelete() === option.value.id) { sdk.client.session.delete({ - sessionID: option.value, + sessionID: option.value.id, }) setToDelete(undefined) return } - setToDelete(option.value) + setToDelete(option.value.id) }, }, { keybind: keybind.all.session_rename?.[0], title: "rename", + disabled: !isCurrentProject(), onTrigger: async (option) => { - dialog.replace(() => ) + dialog.replace(() => ) + }, + }, + { + keybind: projectKeybind(), + title: "project", + onTrigger: showProjectSelect, + }, + ]} + /> + ) +} + +function DialogProjectSelect(props: { current: string; onSelect: (project: string) => void; onCancel: () => void }) { + const dialog = useDialog() + const sdk = useSDK() + const keybind = useKeybind() + + const [projects, { refetch }] = createResource(async () => { + const entries = (await sdk.client.project.list().catch(() => ({ data: [] }))).data ?? [] + if (entries.length === 0) return [] + const lists = await Promise.all( + entries.map(async (entry) => { + const client = sdk.createClient(entry.worktree) + return (await client.session.list({ limit: 200 }).catch(() => ({ data: [] }))).data ?? [] + }), + ) + + const map = new Map() + for (const sessions of lists) { + for (const session of sessions) { + const existing = map.get(session.directory) + if (!existing) { + map.set(session.directory, session.time.updated) + continue + } + if (session.time.updated > existing) { + map.set(session.directory, session.time.updated) + } + } + } + + return Array.from(map.entries()) + .map(([directory, updated]) => ({ directory, updated })) + .toSorted((a, b) => b.updated - a.updated) + }) + + const options = createMemo(() => { + const list = projects() + if (!list) return [] + return list.map((entry) => { + const display = entry.directory.replace(Global.Path.home, "~") + const title = path.basename(entry.directory) || display + return { + title, + value: entry.directory, + description: Locale.truncate(display, 60), + footer: projects.loading ? "Refreshing…" : Locale.time(entry.updated), + } + }) + }) + + const currentSelection = createMemo(() => { + const list = projects() + if (!list) return undefined + if (list.some((entry) => entry.directory === props.current)) return props.current + return undefined + }) + + onMount(() => { + dialog.setSize("large") + }) + + useKeyboard((evt) => { + if (evt.name !== "escape") return + evt.preventDefault() + evt.stopPropagation() + props.onCancel() + }) + + return ( + { + props.onSelect(option.value) + }} + keybind={[ + { + keybind: keybind.all.session_project_refresh?.[0], + title: "refresh", + onTrigger: () => { + refetch() }, }, ]} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 718929d445b..ac03bcbed27 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -12,6 +12,8 @@ import { useTerminalDimensions } from "@opentui/solid" import { Locale } from "@/util/locale" import type { PromptInfo } from "./history" import { useFrecency } from "./frecency" +import { useProjectState } from "@tui/context/directory" +import path from "path" function removeLineRange(input: string) { const hashIndex = input.lastIndexOf("#") @@ -77,6 +79,7 @@ export function Autocomplete(props: { const sdk = useSDK() const sync = useSync() const command = useCommandDialog() + const projectState = useProjectState() const { theme } = useTheme() const dimensions = useTerminalDimensions() const frecency = useFrecency() @@ -234,9 +237,11 @@ export function Autocomplete(props: { }) const width = props.anchor().width - 4 + const root = sync.data.path.directory || projectState.current || process.cwd() options.push( ...sortedFiles.map((item): AutocompleteOption => { - let url = `file://${process.cwd()}/${item}` + const urlPath = path.join(root, item) + let url = `file://${urlPath}` let filename = item if (lineRange && !item.endsWith("/")) { filename = `${item}#${lineRange.startLine}${lineRange.endLine ? `-${lineRange.endLine}` : ""}` diff --git a/packages/opencode/src/cli/cmd/tui/context/directory.ts b/packages/opencode/src/cli/cmd/tui/context/directory.ts index 17e5c180a19..0d862c5d1be 100644 --- a/packages/opencode/src/cli/cmd/tui/context/directory.ts +++ b/packages/opencode/src/cli/cmd/tui/context/directory.ts @@ -1,6 +1,7 @@ -import { createMemo } from "solid-js" +import { createMemo, createSignal } from "solid-js" import { useSync } from "./sync" import { Global } from "@/global" +import { createSimpleContext } from "./helper" export function useDirectory() { const sync = useSync() @@ -11,3 +12,24 @@ export function useDirectory() { return result }) } + +export const { use: useProjectState, provider: ProjectProvider } = createSimpleContext({ + name: "ProjectState", + init: (props: { project?: string; onSwitch?: (project: string) => Promise }) => { + const [current, setCurrent] = createSignal(props.project ?? process.cwd()) + + const switchTo = async (project: string) => { + if (project === current()) return true + await props.onSwitch?.(project) + setCurrent(project) + return true + } + + return { + get current() { + return current() + }, + switchTo, + } + }, +}) diff --git a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx index 4c82e594c3e..8bd72cf5efc 100644 --- a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx @@ -77,6 +77,10 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex if (evt.name === "\x1F") { return Keybind.fromParsedKey({ ...evt, name: "_", ctrl: true }, store.leader) } + // Handle Ctrl+G (bell) which some terminals emit as \x07 + if (evt.name === "\x07") { + return Keybind.fromParsedKey({ ...evt, name: "g", ctrl: true }, store.leader) + } return Keybind.fromParsedKey(evt, store.leader) }, match(key: keyof KeybindsConfig, evt: ParsedKey) { diff --git a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx index 3339e7b00d2..70f7aee0712 100644 --- a/packages/opencode/src/cli/cmd/tui/context/sdk.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/sdk.tsx @@ -1,7 +1,7 @@ import { createOpencodeClient, type Event } from "@opencode-ai/sdk/v2" import { createSimpleContext } from "./helper" import { createGlobalEmitter } from "@solid-primitives/event-bus" -import { batch, onCleanup, onMount } from "solid-js" +import { batch, createEffect, createSignal, on, onCleanup, onMount } from "solid-js" export type EventSource = { on: (handler: (event: Event) => void) => () => void @@ -11,12 +11,26 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ name: "SDK", init: (props: { url: string; directory?: string; fetch?: typeof fetch; events?: EventSource }) => { const abort = new AbortController() - const sdk = createOpencodeClient({ - baseUrl: props.url, - signal: abort.signal, - directory: props.directory, - fetch: props.fetch, - }) + const createClient = (directory?: string) => { + return createOpencodeClient({ + baseUrl: props.url, + signal: abort.signal, + directory, + fetch: props.fetch, + }) + } + + const [sdk, setSdk] = createSignal(createClient(props.directory)) + + createEffect( + on( + () => props.directory, + (directory) => { + setSdk(createClient(directory)) + }, + { defer: true }, + ), + ) const emitter = createGlobalEmitter<{ [key in Event["type"]]: Extract @@ -65,7 +79,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ // Fall back to SSE while (true) { if (abort.signal.aborted) break - const events = await sdk.event.subscribe( + const events = await sdk().event.subscribe( {}, { signal: abort.signal, @@ -89,6 +103,13 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ if (timer) clearTimeout(timer) }) - return { client: sdk, event: emitter, url: props.url } + return { + get client() { + return sdk() + }, + createClient, + event: emitter, + url: props.url, + } }, }) diff --git a/packages/opencode/src/cli/cmd/tui/thread.ts b/packages/opencode/src/cli/cmd/tui/thread.ts index 05714268545..3dc958a0395 100644 --- a/packages/opencode/src/cli/cmd/tui/thread.ts +++ b/packages/opencode/src/cli/cmd/tui/thread.ts @@ -140,6 +140,10 @@ export const TuiThreadCommand = cmd({ events = createEventSource(client) } + const switchProject = async (project: string) => { + await client.call("switchProject", { project }) + } + const tuiPromise = tui({ url, fetch: customFetch, @@ -154,6 +158,7 @@ export const TuiThreadCommand = cmd({ onExit: async () => { await client.call("shutdown", undefined) }, + switchProject, }) setTimeout(() => { diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 618bf3b3cb6..e17b72ac7aa 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -1,7 +1,7 @@ import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core" import { useTheme, selectedForeground } from "@tui/context/theme" import { entries, filter, flatMap, groupBy, pipe, take } from "remeda" -import { batch, createEffect, createMemo, For, Show, type JSX, on } from "solid-js" +import { batch, createEffect, createMemo, createSignal, For, Show, type JSX, on } from "solid-js" import { createStore } from "solid-js/store" import { useKeyboard, useTerminalDimensions } from "@opentui/solid" import * as fuzzysort from "fuzzysort" @@ -27,6 +27,9 @@ export interface DialogSelectProps { onTrigger: (option: DialogSelectOption) => void }[] current?: T + selected?: T + initialFilter?: string + initialScrollTop?: number } export interface DialogSelectOption { @@ -44,21 +47,23 @@ export interface DialogSelectOption { export type DialogSelectRef = { filter: string filtered: DialogSelectOption[] + scrollTop: number } export function DialogSelect(props: DialogSelectProps) { const dialog = useDialog() const { theme } = useTheme() - const [store, setStore] = createStore({ + const [store, setStore] = createStore<{ selected: number; filter: string; input: "keyboard" | "mouse" }>({ selected: 0, - filter: "", - input: "keyboard" as "keyboard" | "mouse", + filter: props.initialFilter ?? "", + input: "keyboard", }) createEffect( on( () => props.current, (current) => { + if (props.selected) return if (current) { const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current)) if (currentIndex >= 0) { @@ -69,6 +74,36 @@ export function DialogSelect(props: DialogSelectProps) { ), ) + const [selectedApplied, setSelectedApplied] = createSignal(false) + const [scrollTopApplied, setScrollTopApplied] = createSignal(false) + + createEffect( + on( + () => props.selected, + (selected) => { + if (!selected) { + setSelectedApplied(false) + return + } + setSelectedApplied(false) + }, + ), + ) + + createEffect( + on([() => props.selected, () => flat().length], ([selected]) => { + if (!selected || selectedApplied()) return + const selectedIndex = flat().findIndex((opt) => isDeepEqual(opt.value, selected)) + if (selectedIndex < 0) return + setStore("selected", selectedIndex) + setSelectedApplied(true) + setTimeout(() => { + if (!scroll) return + moveTo(selectedIndex) + }, 0) + }), + ) + let input: InputRenderable const filtered = createMemo(() => { @@ -121,11 +156,12 @@ export function DialogSelect(props: DialogSelectProps) { setTimeout(() => { if (filter.length > 0) { moveTo(0, true) - } else if (current) { - const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current)) - if (currentIndex >= 0) { - moveTo(currentIndex, true) - } + return + } + if (!current) return + const currentIndex = flat().findIndex((opt) => isDeepEqual(opt.value, current)) + if (currentIndex >= 0) { + moveTo(currentIndex, true) } }, 0) }), @@ -151,16 +187,19 @@ export function DialogSelect(props: DialogSelectProps) { if (center) { const centerOffset = Math.floor(scroll.height / 2) scroll.scrollBy(y - centerOffset) - } else { - if (y >= scroll.height) { - scroll.scrollBy(y - scroll.height + 1) - } - if (y < 0) { - scroll.scrollBy(y) - if (isDeepEqual(flat()[0].value, selected()?.value)) { - scroll.scrollTo(0) - } + if (isDeepEqual(flat()[0].value, selected()?.value)) { + scroll.scrollTo(0) } + return + } + if (y >= scroll.height) { + scroll.scrollBy(y - scroll.height + 1) + } + if (y < 0) { + scroll.scrollBy(y) + } + if (isDeepEqual(flat()[0].value, selected()?.value)) { + scroll.scrollTo(0) } } @@ -205,6 +244,9 @@ export function DialogSelect(props: DialogSelectProps) { get filtered() { return filtered() }, + get scrollTop() { + return scroll?.scrollTop ?? 0 + }, } props.ref?.(ref) @@ -232,6 +274,9 @@ export function DialogSelect(props: DialogSelectProps) { focusedTextColor={theme.textMuted} ref={(r) => { input = r + if (props.initialFilter) { + input.value = props.initialFilter + } setTimeout(() => input.focus(), 1) }} placeholder={props.placeholder ?? "Search"} @@ -250,7 +295,15 @@ export function DialogSelect(props: DialogSelectProps) { paddingLeft={1} paddingRight={1} scrollbarOptions={{ visible: false }} - ref={(r: ScrollBoxRenderable) => (scroll = r)} + ref={(r: ScrollBoxRenderable) => { + scroll = r + if (props.initialScrollTop === undefined || props.selected || scrollTopApplied()) return + setTimeout(() => { + if (!scroll || props.initialScrollTop === undefined) return + scroll.scrollTop = props.initialScrollTop + setScrollTopApplied(true) + }, 1) + }} maxHeight={height()} > diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts index e63f10ba80c..064e6e6e248 100644 --- a/packages/opencode/src/cli/cmd/tui/worker.ts +++ b/packages/opencode/src/cli/cmd/tui/worker.ts @@ -121,6 +121,9 @@ export const rpc = { server = Server.listen(input) return { url: server.url.toString() } }, + async switchProject(input: { project: string }) { + startEventStream(input.project) + }, async checkUpgrade(input: { directory: string }) { await Instance.provide({ directory: input.directory, diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ddb3af4b0a8..1c67c82f221 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -640,6 +640,8 @@ export namespace Config { session_export: z.string().optional().default("x").describe("Export session to editor"), session_new: z.string().optional().default("n").describe("Create a new session"), session_list: z.string().optional().default("l").describe("List all sessions"), + session_project: z.string().optional().default("ctrl+g").describe("Open session project selector"), + session_project_refresh: z.string().optional().default("ctrl+r").describe("Refresh session project list"), session_timeline: z.string().optional().default("g").describe("Show session timeline"), session_fork: z.string().optional().default("none").describe("Fork session from message"), session_rename: z.string().optional().default("ctrl+r").describe("Rename session"), diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index ca13e5e93cf..08d4cefe89e 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -814,6 +814,14 @@ export type KeybindsConfig = { * List all sessions */ session_list?: string + /** + * Open session project selector + */ + session_project?: string + /** + * Refresh session project list + */ + session_project_refresh?: string /** * Show session timeline */ diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index b7e72fbad8f..426a404407a 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -970,6 +970,14 @@ export type KeybindsConfig = { * List all sessions */ session_list?: string + /** + * Open session project selector + */ + session_project?: string + /** + * Refresh session project list + */ + session_project_refresh?: string /** * Show session timeline */