diff --git a/packages/plugin/src/tui/context.ts b/packages/plugin/src/tui/context.ts index 374e56aabe..5f775167ff 100644 --- a/packages/plugin/src/tui/context.ts +++ b/packages/plugin/src/tui/context.ts @@ -351,6 +351,25 @@ export interface UI { navigate(destination: Destination): void current(): Route } + readonly tabs: { + /** Returns whether session tabs are enabled for this TUI. */ + enabled(): boolean + /** Returns the currently open root-session tabs. Reactive when read in a Solid computation. */ + list(): readonly { + readonly sessionID: string + readonly title?: string + readonly active: boolean + readonly busy: boolean + readonly attention: boolean + readonly unread?: "activity" | "error" + }[] + /** Opens (or focuses) a tab for a session, adding it when not already open. Returns false when tabs are disabled. */ + open(sessionID: string): boolean + /** Focuses an already-open tab and returns false when it is not open. */ + focus(sessionID: string): boolean + /** Closes an open tab, or the active tab when omitted, and returns false when no tab matched. */ + close(sessionID?: string): boolean + } readonly slot: (name: Name, render: Slot) => () => void } diff --git a/packages/tui/src/plugin/context.tsx b/packages/tui/src/plugin/context.tsx index 6b9d019db1..0aa465c6fa 100644 --- a/packages/tui/src/plugin/context.tsx +++ b/packages/tui/src/plugin/context.tsx @@ -33,6 +33,7 @@ import { useDialog } from "../ui/dialog" import { useToast } from "../ui/toast" import { useAttention } from "../context/attention" import { useStorage } from "../context/storage" +import { useSessionTabs } from "../context/session-tabs" import { abbreviateHome } from "../util/path-format" import { builtins } from "./builtins" import { discoverTuiPlugins } from "./discovery" @@ -94,6 +95,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }> const toast = useToast() const attention = useAttention() const storage = useStorage() + const sessionTabs = useSessionTabs() const directory = config.path ? path.dirname(config.path) : process.cwd() const [store, setStore] = createStore({ ready: false, @@ -278,6 +280,33 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }> return route.data }, }, + tabs: { + enabled: sessionTabs.enabled, + list: () => + sessionTabs.tabs().map((tab) => ({ + ...tab, + active: sessionTabs.current() === tab.sessionID, + ...sessionTabs.status(tab.sessionID), + })), + open(sessionID) { + if (!sessionTabs.enabled()) return false + sessionTabs.select(sessionID) + return true + }, + focus(sessionID) { + if (!sessionTabs.enabled()) return false + if (!sessionTabs.tabs().some((tab) => tab.sessionID === sessionID)) return false + sessionTabs.select(sessionID) + return true + }, + close(sessionID) { + if (!sessionTabs.enabled()) return false + const target = sessionID ?? sessionTabs.current() + if (!target || !sessionTabs.tabs().some((tab) => tab.sessionID === target)) return false + sessionTabs.close(target) + return true + }, + }, slot(name, render) { if (store.registrations[item.plugin.id]?.slots[name]) throw new Error(`Slot already registered: ${name}`) setStore("registrations", item.plugin.id, "slots", name, () => (input: SlotMap[typeof name]) => (