fix(tui): smooth new session tab handoff (#39745)

This commit is contained in:
Kit Langton 2026-07-30 16:17:01 -04:00 committed by GitHub
commit 16b247f756
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 21 additions and 3 deletions

View file

@ -6,6 +6,7 @@ import { useSessionTabs } from "../context/session-tabs"
import { useTheme, useThemes } from "../context/theme"
import {
adaptiveSessionTabLayout,
NEW_SESSION_TAB_TITLE,
sessionTabComplete,
seedSessionTabMotion,
sessionTabOverflowWidth,
@ -36,7 +37,7 @@ export type SessionTabsController = Pick<ContextController, "tabs" | "current" |
status(sessionID: string): SessionTabsStatus
}
const NEW_SESSION_TAB: SessionTab = { sessionID: "new", title: "New session" }
const NEW_SESSION_TAB: SessionTab = { sessionID: "new", title: NEW_SESSION_TAB_TITLE }
export function SessionTabs(props: { controller?: SessionTabsController; animations?: boolean } = {}) {
const tabs = props.controller ?? useSessionTabs()
@ -209,6 +210,11 @@ export function SessionTabs(props: { controller?: SessionTabsController; animati
createEffect((previous: string) => {
const next = title()
if (next === previous) return next
if (previous === NEW_SESSION_TAB_TITLE) {
setOutgoingTitle(undefined)
wipe.jump({ front: 1 })
return next
}
setOutgoingTitle(previous)
wipe.jump({ front: 0 })
wipe.animate({ front: 1 })

View file

@ -5,6 +5,8 @@ export type SessionTab = {
export type SessionTabUnread = "activity" | "error"
export const NEW_SESSION_TAB_TITLE = "New session"
export type SessionTabHistory = {
entries: readonly string[]
index: number

View file

@ -13,6 +13,7 @@ import {
cycleSessionTab,
moveSessionTab,
moveSessionTabHistory,
NEW_SESSION_TAB_TITLE,
openSessionTab,
recordClosedSessionTab,
recordSessionTabHistory,
@ -77,6 +78,12 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
const root = (sessionID: string) => data.session.root(sessionID)
const current = () => (route.data.type === "session" ? root(route.data.sessionID) : undefined)
const newTab = createMemo((open = false) => {
if (route.data.type === "home") return true
if (!open) return false
const sessionID = current()
return sessionID !== undefined && !state().tabs.some((tab) => tab.sessionID === sessionID)
}, false)
const status = (sessionID: string) => {
const session = root(sessionID)
const members = data.session.family(session)
@ -107,7 +114,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
if (route.data.type !== "session" || route.data.sessionID === "dummy") return
const sessionID = root(route.data.sessionID)
history = recordSessionTabHistory(history, sessionID)
const title = data.session.get(sessionID)?.title
const title = data.session.get(sessionID)?.title ?? (newTab() ? NEW_SESSION_TAB_TITLE : undefined)
const tabs = openSessionTab(state().tabs, { sessionID, title })
if (tabs === state().tabs && !state().unread[sessionID]) return
update((draft) => {
@ -233,7 +240,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
return state().tabs
},
newTab() {
return route.data.type === "home"
return newTab()
},
current,
status,

View file

@ -11,6 +11,7 @@ import { DataProvider } from "../../src/context/data"
import { RouteProvider, useRoute } from "../../src/context/route"
import { TuiAppProvider } from "../../src/context/runtime"
import { SessionTabsProvider, useSessionTabs } from "../../src/context/session-tabs"
import { NEW_SESSION_TAB_TITLE } from "../../src/context/session-tabs-model"
import { StorageProvider } from "../../src/context/storage"
import { createApi, createEventStream, createFetch, directory } from "../fixture/tui-client"
import { TestTuiContexts } from "../fixture/tui-environment"
@ -142,11 +143,13 @@ test("tracks a temporary new session tab across close and creation", async () =>
setup.route.navigate({ type: "home" })
await wait(() => setup.tabs.newTab())
setup.route.navigate({ type: "session", sessionID: "third" })
expect(setup.tabs.newTab()).toBe(true)
await wait(
() => setup.tabs.current() === "third" && setup.tabs.tabs().some((tab) => tab.sessionID === "third"),
)
expect(setup.tabs.newTab()).toBe(false)
expect(setup.tabs.tabs().find((tab) => tab.sessionID === "third")?.title).toBe(NEW_SESSION_TAB_TITLE)
} finally {
setup.destroy()
}