feat(session): make generated titles optional (#39747)

This commit is contained in:
Kit Langton 2026-07-30 21:24:34 -04:00 committed by GitHub
commit eb95bd27fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 215 additions and 64 deletions

View file

@ -11,6 +11,7 @@ import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
import type { Session } from "@opencode-ai/sdk/v2"
import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture"
import { TabPreviewPopover } from "./titlebar-tab-popover"
import { sessionTitle } from "@/utils/session-title"
import "./titlebar-tab-nav.css"
// MouseEvent.button uses 1 for the middle/wheel button.
@ -54,7 +55,10 @@ export function TabNavItem(props: {
if (!session) return
return projectForSession(session, serverCtx()?.projects.list() ?? [])
})
const title = createMemo(() => props.session()?.title ?? props.fallbackTitle)
const title = createMemo(() => {
const session = props.session()
return session ? sessionTitle(session.title, session.parentID) : props.fallbackTitle
})
const projectName = createMemo(() => {
const session = props.session()
@ -143,7 +147,7 @@ export function TabNavItem(props: {
if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return
const session = props.session()
if (!session) return
titleEl.textContent = session.title
titleEl.textContent = session.title ?? ""
setEditing(true)
requestAnimationFrame(() => {
@ -302,7 +306,7 @@ export function TabNavItem(props: {
}}
data={{
projectName: projectName(),
title: props.session()?.title,
title: title(),
path: previewPath(),
serverName: serverLabel(),
}}

View file

@ -7,6 +7,7 @@ import { createMemo, onCleanup } from "solid-js"
import { createStore } from "solid-js/store"
import type { HomeController } from "./home-controller"
import { homeSessionSearchKey, type HomeSessionRecord, type HomeSessionsController } from "./home-sessions-controller"
import { sessionTitle } from "@/utils/session-title"
type HomeSessionSearchSource = Pick<HomeSessionsController, "data" | "session">
@ -23,7 +24,7 @@ export function createHomeSessionSearchController(home: HomeController, sessions
if (!value) return []
return sessions.data
.searchRecords()
.filter((record) => `${record.session.title} ${record.projectName}`.toLowerCase().includes(value))
.filter((record) => `${sessionTitle(record.session.title)} ${record.projectName}`.toLowerCase().includes(value))
})
const active = createMemo(() => {
const records = results()

View file

@ -104,7 +104,7 @@ const SessionRow = (props: {
warmPress: () => void
warmFocus: () => void
}): JSX.Element => {
const title = () => sessionTitle(props.session.title)
const title = () => sessionTitle(props.session.title, props.session.parentID)
return (
<A
@ -229,7 +229,7 @@ export const SessionItem = (props: SessionItemProps): JSX.Element => {
fallback={
<Tooltip
placement={props.mobile ? "bottom" : "right"}
value={sessionTitle(props.session.title)}
value={sessionTitle(props.session.title, props.session.parentID)}
gutter={10}
class="min-w-0 w-full"
>

View file

@ -297,7 +297,11 @@ export function MessageTimeline(props: {
return sync().session.get(id)
})
const titleValue = createMemo(() => info()?.title)
const titleLabel = createMemo(() => sessionTitle(titleValue()))
const titleLabel = createMemo(() => {
const session = info()
if (!session) return
return sessionTitle(titleValue(), session.parentID)
})
const shareUrl = createMemo(() => info()?.share?.url)
const shareEnabled = createMemo(() => sync().data.config.share !== "disabled")
const parentID = createMemo(() => info()?.parentID)
@ -311,7 +315,10 @@ export function MessageTimeline(props: {
if (!id) return emptyMessages
return sync().data.message[id] ?? emptyMessages
})
const parentTitle = createMemo(() => sessionTitle(parent()?.title) ?? language.t("command.session.new"))
const parentTitle = createMemo(() => {
const session = parent()
return session ? sessionTitle(session.title, session.parentID) : language.t("command.session.new")
})
const getMsgParts = (msgId: string) => sync().data.part[msgId] ?? emptyParts
const getMsgPart = (messageID: string, partID: string) => getMsgParts(messageID).find((part) => part.id === partID)
const childTaskDescription = createMemo(() => {
@ -329,7 +336,7 @@ export function MessageTimeline(props: {
if (value) return value
return language.t("command.session.new")
})
const showHeader = createMemo(() => !!(titleValue() || parentID()))
const showHeader = createMemo(() => !!(titleLabel() || parentID()))
const projection = createTimelineProjection({
messages: sessionMessages,
userMessages: () => props.userMessages,
@ -912,9 +919,10 @@ export function MessageTimeline(props: {
}
function DialogDeleteSession(props: { sessionID: string }) {
const name = createMemo(
() => sessionTitle(sync().session.get(props.sessionID)?.title) ?? language.t("command.session.new"),
)
const name = createMemo(() => {
const session = sync().session.get(props.sessionID)
return session ? sessionTitle(session.title, session.parentID) : language.t("command.session.new")
})
const handleDelete = async () => {
await deleteSession(props.sessionID)
dialog.close()

View file

@ -0,0 +1,11 @@
import { describe, expect, test } from "bun:test"
import { sessionTitle } from "./session-title"
describe("sessionTitle", () => {
test("uses a display fallback without persisting it", () => {
expect(sessionTitle(undefined)).toBe("New session")
expect(sessionTitle(undefined, "ses_parent")).toBe("Child session")
expect(sessionTitle("New session - 2026-07-30T18:45:03.662Z")).toBe("New session")
expect(sessionTitle("Generated title")).toBe("Generated title")
})
})

View file

@ -1,7 +1,7 @@
const pattern = /^(New session|Child session) - \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
export function sessionTitle(title?: string) {
if (!title) return title
export function sessionTitle(title?: string, parentID?: string) {
if (!title) return parentID ? "Child session" : "New session"
const match = title.match(pattern)
return match?.[1] ?? title
}

View file

@ -13,7 +13,7 @@ export function normalizeSessionInfo(input: SessionInfo | Session): Session {
parentID: input.parentID,
cost: input.cost,
tokens: input.tokens,
title: input.title,
title: input.title ?? `${input.parentID ? "Child" : "New"} session - ${new Date(input.time.created).toISOString()}`,
agent: input.agent,
model: input.model,
version: "",