feat(tui): polish session tab animations (#39542)

This commit is contained in:
Kit Langton 2026-07-29 13:50:49 -04:00 committed by GitHub
commit b985d2eb8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 405 additions and 107 deletions

View file

@ -1,6 +1,11 @@
import { expect, test } from "bun:test"
import { RGBA } from "@opentui/core"
import { blendTabPulseColor, completionPulseOpacity, unreadGlowIntensity } from "../../src/component/tab-pulse"
import {
blendTabPulseColor,
completionPulseOpacity,
glowIgnitionLevel,
unreadGlowIntensity,
} from "../../src/component/tab-pulse"
import { tint } from "../../src/theme/color"
test("completion pulse rises quickly and fades over the remaining duration", () => {
@ -11,6 +16,13 @@ test("completion pulse rises quickly and fades over the remaining duration", ()
expect(completionPulseOpacity(1)).toBe(0)
})
test("glow ignition overshoots the resting level and settles back to it", () => {
expect(glowIgnitionLevel(0)).toBe(0)
expect(glowIgnitionLevel(0.3)).toBeCloseTo(1.5)
expect(glowIgnitionLevel(0.6)).toBeGreaterThan(1)
expect(glowIgnitionLevel(1)).toBe(1)
})
test("unread glow peaks behind the tab number and fades to the normal background", () => {
const intensities = Array.from({ length: 22 }, (_, index) => unreadGlowIntensity(index, 22))

View file

@ -3,13 +3,65 @@ import {
adaptiveSessionTabLayout,
closeSessionTab,
cycleSessionTab,
moveSessionTab,
moveSessionTabHistory,
openSessionTab,
recordSessionTabHistory,
seedSessionTabMotion,
sessionTabComplete,
sessionTabOverflowWidth,
} from "../../src/context/session-tabs-model"
describe("session tabs", () => {
test("moves a tab to a clamped index and returns the same tabs for no-ops", () => {
const tabs = ["a", "b", "c"].map((sessionID) => ({ sessionID }))
expect(moveSessionTab(tabs, "a", 2).map((tab) => tab.sessionID)).toEqual(["b", "c", "a"])
expect(moveSessionTab(tabs, "c", -5).map((tab) => tab.sessionID)).toEqual(["c", "a", "b"])
expect(moveSessionTab(tabs, "b", 99).map((tab) => tab.sessionID)).toEqual(["a", "c", "b"])
expect(moveSessionTab(tabs, "b", 1)).toBe(tabs)
expect(moveSessionTab(tabs, "missing", 0)).toBe(tabs)
})
test("open seeding keeps survivors and grows the new tab from zero", () => {
const seeded = seedSessionTabMotion(
["a", "b"],
["a", "b", "c"],
{ widths: [35, 35], selections: [1, 0], activities: [0, 1] },
{ widths: [24, 23, 23], selections: [1, 0, 0], activities: [0, 1, 0] },
)
expect(seeded).toEqual({ widths: [35, 35, 0], selections: [1, 0, 0], activities: [0, 1, 0] })
})
test("close seeding keeps survivors at their current animated widths", () => {
const seeded = seedSessionTabMotion(
["a", "b", "c"],
["a", "c"],
{ widths: [24, 23, 23], selections: [1, 0, 0], activities: [0, 0, 1] },
{ widths: [35, 35], selections: [1, 0], activities: [0, 1] },
)
expect(seeded).toEqual({ widths: [24, 23], selections: [1, 0], activities: [0, 1] })
})
test("window shifts keep retained tabs and grow revealed ones", () => {
const seeded = seedSessionTabMotion(
["a", "b", "c"],
["b", "c", "d"],
{ widths: [22, 8, 8], selections: [1, 0, 0], activities: [0, 0, 0] },
{ widths: [8, 8, 22], selections: [0, 0, 1], activities: [0, 0, 0] },
)
expect(seeded).toEqual({ widths: [8, 8, 0], selections: [0, 0, 1], activities: [0, 0, 0] })
})
test("fully replaced windows jump instead of seeding", () => {
const values = { widths: [22], selections: [1], activities: [0] }
expect(seedSessionTabMotion(["a"], ["z"], values, values)).toBeUndefined()
})
test("overflow markers reserve room for a gap beside their digits", () => {
expect(sessionTabOverflowWidth(5)).toBe(3)
expect(sessionTabOverflowWidth(12)).toBe(4)
})
test("opens each session once and refreshes its title", () => {
const tabs = openSessionTab([{ sessionID: "a", title: "Old" }], { sessionID: "a", title: "New" })
expect(tabs).toEqual([{ sessionID: "a", title: "New" }])