diff --git a/packages/tui/src/component/session-tabs.tsx b/packages/tui/src/component/session-tabs.tsx index b111af5a89..4bbd4590fc 100644 --- a/packages/tui/src/component/session-tabs.tsx +++ b/packages/tui/src/component/session-tabs.tsx @@ -16,6 +16,7 @@ import { type SessionTab, type SessionTabUnread, } from "../context/session-tabs-model" +import { isFallbackTitle } from "@opencode-ai/util/session-title-fallback" import { createAnimatable, spring, tween } from "../ui/animation" import { Locale } from "../util/locale" import { stringWidth } from "../util/string-width" @@ -60,6 +61,11 @@ function fadeTitleColor(color: RGBA, background: RGBA, index: number, length: nu return opacity === 0 ? color : tint(color, background, opacity) } +// A tab title is provisional until the session earns a generated or user-provided one. +function isPlaceholderSessionTitle(value: string | undefined) { + return value === NEW_SESSION_TAB_TITLE || value === "Untitled session" || isFallbackTitle(value) +} + function createMarquee(hovered: () => string | undefined, animations: () => boolean) { const [offset, setOffset] = createSignal(0) const leading = createAnimatable({ opacity: 0 }, { enabled: animations, transition: tween({ duration: 0.25 }) }) @@ -185,6 +191,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat const numberWidth = () => 2 const titleWidth = () => Math.max(1, width() - numberWidth() - 2 - (hovered() === tab.sessionID ? 1 : 0)) const title = () => tab.title ?? "Untitled session" + const placeholder = () => isPlaceholderSessionTitle(tab.title) const scrolling = () => hovered() === tab.sessionID && marquee.offset() > 0 const visibleTitle = createMemo(() => scrolling() @@ -215,8 +222,10 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat return sweepLevel() === 0 ? color : tint(color, theme.text.default, 0.15 * sweepLevel()) } const foreground = () => { - if (hovered() === tab.sessionID) return theme.text.default - return selected() ? theme.text.default : theme.text.subdued + const base = + hovered() === tab.sessionID || selected() ? theme.text.default : theme.text.subdued + // A provisional title reads dimmer than its neighbors until the real one arrives. + return placeholder() ? tint(base, pulseBackground(), 0.35) : base } const complete = () => status().complete const glowHue = () => { @@ -373,7 +382,10 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat fg={foreground()} wrapMode="none" selectable={false} - attributes={selected() ? TextAttributes.BOLD : undefined} + attributes={ + (selected() ? TextAttributes.BOLD : 0) | (placeholder() ? TextAttributes.ITALIC : 0) || + undefined + } > @@ -676,6 +688,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim const glowColor = () => feedbackColor() ?? accent() const glows = () => !selected() && (status().attention || (!status().busy && status().unread !== undefined)) const title = () => tab.title ?? "Untitled session" + const placeholder = () => tab !== NEW_SESSION_TAB && isPlaceholderSessionTitle(tab.title) const tabNumber = createMemo(() => items().findIndex((item) => item.sessionID === tab.sessionID) + 1) // Shortcut labels stay one cell wide: 1-9, 0 for ten, then a neutral dot. const numberWidth = () => 2 @@ -693,8 +706,10 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim () => stringWidth(title()) >= availableTitleWidth() && availableTitleWidth() > FADE_WIDTH, ) const foreground = () => { - if (hovered() === tab.sessionID) return theme.text.default - return tint(theme.text.subdued, theme.text.default, selection()) + const base = + hovered() === tab.sessionID ? theme.text.default : tint(theme.text.subdued, theme.text.default, selection()) + // A provisional title reads dimmer than its neighbors until the real one arrives. + return placeholder() ? tint(base, background(), 0.35) : base } // Title characters sitting over the glow tinge toward its color, following the same // spatial falloff as the glow itself; characters beyond the tail stay neutral. @@ -782,7 +797,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim fg={foreground()} wrapMode="none" selectable={false} - attributes={bold()} + attributes={(bold() ?? 0) | (placeholder() ? TextAttributes.ITALIC : 0) || undefined} >