feat(tui): add temporary new session tab (#39735)
This commit is contained in:
parent
188c642d8c
commit
e1c04dcce6
6 changed files with 107 additions and 37 deletions
|
|
@ -1219,7 +1219,13 @@ function App(props: { pair?: DialogPairCredentials }) {
|
|||
<box flexGrow={1} minWidth={0} flexDirection="column">
|
||||
<Show when={plugins.ready()}>
|
||||
<box flexGrow={1} minHeight={0} flexDirection="column">
|
||||
<Show when={sessionTabs.enabled() && sessionTabs.tabs().length > 0 && route.data.type !== "plugin"}>
|
||||
<Show
|
||||
when={
|
||||
sessionTabs.enabled() &&
|
||||
(sessionTabs.tabs().length > 0 || sessionTabs.newTab()) &&
|
||||
route.data.type !== "plugin"
|
||||
}
|
||||
>
|
||||
<SessionTabs />
|
||||
</Show>
|
||||
<Switch>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
sessionTabComplete,
|
||||
seedSessionTabMotion,
|
||||
sessionTabOverflowWidth,
|
||||
type SessionTab,
|
||||
type SessionTabUnread,
|
||||
} from "../context/session-tabs-model"
|
||||
import { createAnimatable, spring, tween } from "../ui/animation"
|
||||
|
|
@ -24,10 +25,19 @@ type ContextController = ReturnType<typeof useSessionTabs>
|
|||
export type SessionTabsStatus = Omit<ReturnType<ContextController["status"]>, "unread"> & {
|
||||
unread: SessionTabUnread | undefined
|
||||
}
|
||||
export const EMPTY_SESSION_TAB_STATUS: SessionTabsStatus = {
|
||||
unread: undefined,
|
||||
promptPulse: 0,
|
||||
attention: false,
|
||||
busy: false,
|
||||
}
|
||||
export type SessionTabsController = Pick<ContextController, "tabs" | "current" | "select" | "close" | "move"> & {
|
||||
newTab?: () => boolean
|
||||
status(sessionID: string): SessionTabsStatus
|
||||
}
|
||||
|
||||
const NEW_SESSION_TAB: SessionTab = { sessionID: "new", title: "New session" }
|
||||
|
||||
export function SessionTabs(props: { controller?: SessionTabsController; animations?: boolean } = {}) {
|
||||
const tabs = props.controller ?? useSessionTabs()
|
||||
const dimensions = useTerminalDimensions()
|
||||
|
|
@ -42,8 +52,9 @@ export function SessionTabs(props: { controller?: SessionTabsController; animati
|
|||
const accent = () => theme.hue.accent[hueStep()]
|
||||
const activeNumber = () => theme.hue.interactive[hueStep()]
|
||||
const idleNumber = () => tint(theme.text.subdued, theme.background.default, 0.35)
|
||||
const activeID = createMemo(tabs.current)
|
||||
const items = tabs.tabs
|
||||
const newTab = () => tabs.newTab?.() ?? false
|
||||
const activeID = createMemo(() => (newTab() ? NEW_SESSION_TAB.sessionID : tabs.current()))
|
||||
const items = createMemo(() => (newTab() ? [...tabs.tabs(), NEW_SESSION_TAB] : tabs.tabs()))
|
||||
const layout = createMemo((previous: ReturnType<typeof adaptiveSessionTabLayout> | undefined) =>
|
||||
adaptiveSessionTabLayout(items(), activeID(), dimensions().width, previous?.start),
|
||||
)
|
||||
|
|
@ -51,7 +62,7 @@ export function SessionTabs(props: { controller?: SessionTabsController; animati
|
|||
() =>
|
||||
new Map(
|
||||
layout().tabs.map((tab) => {
|
||||
const status = tabs.status(tab.sessionID)
|
||||
const status = tab === NEW_SESSION_TAB ? EMPTY_SESSION_TAB_STATUS : tabs.status(tab.sessionID)
|
||||
return [
|
||||
tab.sessionID,
|
||||
{
|
||||
|
|
@ -262,6 +273,7 @@ export function SessionTabs(props: { controller?: SessionTabsController; animati
|
|||
// keeping sloppy clicks indistinguishable from clean ones.
|
||||
const release = () => {
|
||||
setDragging(undefined)
|
||||
if (tab === NEW_SESSION_TAB) return
|
||||
tabs.select(tab.sessionID)
|
||||
}
|
||||
return (
|
||||
|
|
@ -275,6 +287,7 @@ export function SessionTabs(props: { controller?: SessionTabsController; animati
|
|||
onMouseDown={() => setDragging(tab.sessionID)}
|
||||
onMouseUp={release}
|
||||
onMouseDrag={(event) => {
|
||||
if (tab === NEW_SESSION_TAB) return
|
||||
const slot = slotAt(event.x)
|
||||
if (slot !== undefined && slot !== tabNumber() - 1) tabs.move(tab.sessionID, slot)
|
||||
}}
|
||||
|
|
@ -323,7 +336,7 @@ export function SessionTabs(props: { controller?: SessionTabsController; animati
|
|||
selectable={false}
|
||||
onMouseUp={(event) => {
|
||||
event.stopPropagation()
|
||||
tabs.close(tab.sessionID)
|
||||
tabs.close(tab === NEW_SESSION_TAB ? undefined : tab.sessionID)
|
||||
}}
|
||||
>
|
||||
{hovered() === tab.sessionID ? "×" : ""}
|
||||
|
|
|
|||
|
|
@ -225,6 +225,9 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
|||
tabs() {
|
||||
return state().tabs
|
||||
},
|
||||
newTab() {
|
||||
return route.data.type === "home"
|
||||
},
|
||||
current,
|
||||
status,
|
||||
select(sessionID: string) {
|
||||
|
|
@ -235,8 +238,10 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
|||
if (!enabled()) return
|
||||
const target = sessionID ? root(sessionID) : current()
|
||||
if (!target) {
|
||||
const previous = state().tabs.at(-1)
|
||||
if (route.data.type === "home" && previous) route.navigate({ type: "session", sessionID: previous.sessionID })
|
||||
const previous = moveSessionTabHistory(history, state().tabs, undefined, -1)
|
||||
history = previous.history
|
||||
const session = previous.sessionID ?? state().tabs.at(-1)?.sessionID
|
||||
if (route.data.type === "home" && session) route.navigate({ type: "session", sessionID: session })
|
||||
return
|
||||
}
|
||||
remove(target, true)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import { Plugin } from "@opencode-ai/plugin/tui"
|
|||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { batch, createSignal, For, onCleanup } from "solid-js"
|
||||
import { createStore, reconcile } from "solid-js/store"
|
||||
import { SessionTabs, type SessionTabsController } from "../../../component/session-tabs"
|
||||
import {
|
||||
EMPTY_SESSION_TAB_STATUS,
|
||||
SessionTabs,
|
||||
type SessionTabsController,
|
||||
} from "../../../component/session-tabs"
|
||||
import { moveSessionTab } from "../../../context/session-tabs-model"
|
||||
import type { Story } from "./index"
|
||||
|
||||
|
|
@ -23,7 +27,6 @@ const FIXTURE_TABS = [
|
|||
{ sessionID: "fixture-12", title: "Prepare review" },
|
||||
]
|
||||
|
||||
const EMPTY_STATUS: FixtureStatus = { unread: undefined, promptPulse: 0, attention: false, busy: false }
|
||||
const RUN_DURATION = 1_800
|
||||
const RESUME_DURATION = 900
|
||||
|
||||
|
|
@ -66,7 +69,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
|
|||
if (!resumed && roll < 0.25) {
|
||||
setStatuses((current) => ({
|
||||
...current,
|
||||
[sessionID]: { ...(current[sessionID] ?? EMPTY_STATUS), attention: true },
|
||||
[sessionID]: { ...(current[sessionID] ?? EMPTY_SESSION_TAB_STATUS), attention: true },
|
||||
}))
|
||||
setLastEvent(`tab ${number(sessionID)} needs input; select it to resolve`)
|
||||
return
|
||||
|
|
@ -77,7 +80,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
|
|||
setOutcomes((current) => ({ ...current, [sessionID]: failed ? "failed" : "completed" }))
|
||||
setStatuses((current) => ({
|
||||
...current,
|
||||
[sessionID]: { ...(current[sessionID] ?? EMPTY_STATUS), busy: false, unread },
|
||||
[sessionID]: { ...(current[sessionID] ?? EMPTY_SESSION_TAB_STATUS), busy: false, unread },
|
||||
}))
|
||||
// An untitled session earns its title after its first completed run, like a real summarization.
|
||||
const index = number(sessionID) - 1
|
||||
|
|
@ -110,7 +113,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
|
|||
tabs,
|
||||
current: active,
|
||||
status(sessionID) {
|
||||
return statuses()[sessionID] ?? EMPTY_STATUS
|
||||
return statuses()[sessionID] ?? EMPTY_SESSION_TAB_STATUS
|
||||
},
|
||||
select,
|
||||
move(sessionID: string, index: number) {
|
||||
|
|
@ -150,7 +153,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
|
|||
const startRun = (sessionID: string) => {
|
||||
setStatuses((current) => ({
|
||||
...current,
|
||||
[sessionID]: { ...(current[sessionID] ?? EMPTY_STATUS), busy: true, unread: undefined },
|
||||
[sessionID]: { ...(current[sessionID] ?? EMPTY_SESSION_TAB_STATUS), busy: true, unread: undefined },
|
||||
}))
|
||||
setOutcomes((current) => {
|
||||
const next = { ...current }
|
||||
|
|
@ -223,7 +226,7 @@ function SessionTabsStory(props: { context: Plugin.Context }) {
|
|||
|
||||
const selectedState = () => {
|
||||
const current = active()
|
||||
const status = current ? controller.status(current) : EMPTY_STATUS
|
||||
const status = current ? controller.status(current) : EMPTY_SESSION_TAB_STATUS
|
||||
const activity = status.busy
|
||||
? "running"
|
||||
: status.unread === "activity"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue