Skip to content

Commit 78c139b

Browse files
authored
feat(plugin): add ui.tabs API for session tab control (#39591)
1 parent c161978 commit 78c139b

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

packages/plugin/src/tui/context.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,25 @@ export interface UI {
351351
navigate(destination: Destination): void
352352
current(): Route
353353
}
354+
readonly tabs: {
355+
/** Returns whether session tabs are enabled for this TUI. */
356+
enabled(): boolean
357+
/** Returns the currently open root-session tabs. Reactive when read in a Solid computation. */
358+
list(): readonly {
359+
readonly sessionID: string
360+
readonly title?: string
361+
readonly active: boolean
362+
readonly busy: boolean
363+
readonly attention: boolean
364+
readonly unread?: "activity" | "error"
365+
}[]
366+
/** Opens (or focuses) a tab for a session, adding it when not already open. Returns false when tabs are disabled. */
367+
open(sessionID: string): boolean
368+
/** Focuses an already-open tab and returns false when it is not open. */
369+
focus(sessionID: string): boolean
370+
/** Closes an open tab, or the active tab when omitted, and returns false when no tab matched. */
371+
close(sessionID?: string): boolean
372+
}
354373
readonly slot: <Name extends SlotName>(name: Name, render: Slot<Name>) => () => void
355374
}
356375

packages/tui/src/plugin/context.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { useDialog } from "../ui/dialog"
3333
import { useToast } from "../ui/toast"
3434
import { useAttention } from "../context/attention"
3535
import { useStorage } from "../context/storage"
36+
import { useSessionTabs } from "../context/session-tabs"
3637
import { abbreviateHome } from "../util/path-format"
3738
import { builtins } from "./builtins"
3839
import { discoverTuiPlugins } from "./discovery"
@@ -94,6 +95,7 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }>
9495
const toast = useToast()
9596
const attention = useAttention()
9697
const storage = useStorage()
98+
const sessionTabs = useSessionTabs()
9799
const directory = config.path ? path.dirname(config.path) : process.cwd()
98100
const [store, setStore] = createStore({
99101
ready: false,
@@ -278,6 +280,33 @@ export function PluginProvider(props: ParentProps<{ packages: PackageResolver }>
278280
return route.data
279281
},
280282
},
283+
tabs: {
284+
enabled: sessionTabs.enabled,
285+
list: () =>
286+
sessionTabs.tabs().map((tab) => ({
287+
...tab,
288+
active: sessionTabs.current() === tab.sessionID,
289+
...sessionTabs.status(tab.sessionID),
290+
})),
291+
open(sessionID) {
292+
if (!sessionTabs.enabled()) return false
293+
sessionTabs.select(sessionID)
294+
return true
295+
},
296+
focus(sessionID) {
297+
if (!sessionTabs.enabled()) return false
298+
if (!sessionTabs.tabs().some((tab) => tab.sessionID === sessionID)) return false
299+
sessionTabs.select(sessionID)
300+
return true
301+
},
302+
close(sessionID) {
303+
if (!sessionTabs.enabled()) return false
304+
const target = sessionID ?? sessionTabs.current()
305+
if (!target || !sessionTabs.tabs().some((tab) => tab.sessionID === target)) return false
306+
sessionTabs.close(target)
307+
return true
308+
},
309+
},
281310
slot(name, render) {
282311
if (store.registrations[item.plugin.id]?.slots[name]) throw new Error(`Slot already registered: ${name}`)
283312
setStore("registrations", item.plugin.id, "slots", name, () => (input: SlotMap[typeof name]) => (

0 commit comments

Comments
 (0)