refactor(app): extract tab rename adapter (#39079)

This commit is contained in:
Brendan Allan
2026-07-27 17:54:24 +08:00
committed by GitHub
parent 92e65919cd
commit bfb012544e
3 changed files with 29 additions and 41 deletions
@@ -12,6 +12,6 @@ export function forwardTabRef(ref: Ref<HTMLDivElement> | undefined, element: HTM
if (typeof ref === "function") ref(element) if (typeof ref === "function") ref(element)
} }
export function canOpenTabRename(dragging: boolean | undefined, editing: boolean, committing: boolean) { export function canOpenTabRename(dragging: boolean | undefined, editing: boolean, pending: boolean) {
return !dragging && !editing && !committing return !dragging && !editing && !pending
} }
@@ -1,14 +1,13 @@
import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } from "solid-js" import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } from "solid-js"
import { makeEventListener } from "@solid-primitives/event-listener" import { makeEventListener } from "@solid-primitives/event-listener"
import { createResizeObserver } from "@solid-primitives/resize-observer" import { createResizeObserver } from "@solid-primitives/resize-observer"
import { createMutation } from "@tanstack/solid-query"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { useGlobal } from "@/context/global" import { useGlobal } from "@/context/global"
import { useLanguage } from "@/context/language"
import { ServerConnection, serverName } from "@/context/server" import { ServerConnection, serverName } from "@/context/server"
import { displayName, projectForSession } from "@/pages/layout/helpers" import { displayName, projectForSession } from "@/pages/layout/helpers"
import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar" import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar"
import { showToast } from "@/utils/toast"
import type { Session } from "@opencode-ai/sdk/v2" import type { Session } from "@opencode-ai/sdk/v2"
import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture" import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture"
import { TabPreviewPopover } from "./titlebar-tab-popover" import { TabPreviewPopover } from "./titlebar-tab-popover"
@@ -23,8 +22,7 @@ export function TabNavItem(props: {
server: ServerConnection.Key server: ServerConnection.Key
session: () => Session | undefined session: () => Session | undefined
fallbackTitle?: string fallbackTitle?: string
onTitleChange?: (title: string) => void onRename: (title: string) => Promise<void>
onTitleChangeFailed?: (title: string) => void
onClose: () => void onClose: () => void
onNavigate: () => void onNavigate: () => void
active?: boolean active?: boolean
@@ -34,13 +32,12 @@ export function TabNavItem(props: {
pressed?: boolean pressed?: boolean
hidden?: boolean hidden?: boolean
}) { }) {
const language = useLanguage()
const [editing, setEditing] = createSignal(false) const [editing, setEditing] = createSignal(false)
const [titleOverflowing, setTitleOverflowing] = createSignal(false) const [titleOverflowing, setTitleOverflowing] = createSignal(false)
let tabRoot!: HTMLDivElement let tabRoot!: HTMLDivElement
let titleEl!: HTMLSpanElement let titleEl!: HTMLSpanElement
let committing = false
let measureFrame: number | undefined let measureFrame: number | undefined
const rename = createMutation(() => ({ mutationFn: props.onRename }))
const closeTab = (event: MouseEvent) => { const closeTab = (event: MouseEvent) => {
event.preventDefault() event.preventDefault()
@@ -116,40 +113,20 @@ export function TabNavItem(props: {
selection?.addRange(range) selection?.addRange(range)
} }
const rename = async (title: string) => {
const ctx = serverCtx()
const session = props.session()
if (!ctx || !session) return
await ctx.sdk.api.session.rename({ sessionID: session.id, title })
}
const closeRename = async (save: boolean) => { const closeRename = async (save: boolean) => {
if (committing || !editing()) return if (rename.isPending || !editing()) return
committing = true
const original = props.session()?.title ?? "" const original = props.session()?.title ?? ""
const next = (titleEl.textContent ?? "").trim() const next = (titleEl.textContent ?? "").trim()
titleEl.scrollLeft = 0 titleEl.scrollLeft = 0
if (save && next && next !== original) props.onTitleChange?.(next)
setEditing(false) setEditing(false)
if (!save || !next || next === original) { if (!save || !next || next === original) {
committing = false
return return
} }
try { await rename.mutateAsync(next)
await rename(next)
} catch (err) {
props.onTitleChangeFailed?.(original)
showToast({
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : undefined,
})
}
committing = false
} }
createEffect(() => { createEffect(() => {
@@ -163,7 +140,7 @@ export function TabNavItem(props: {
const openRename = (event: MouseEvent) => { const openRename = (event: MouseEvent) => {
event.preventDefault() event.preventDefault()
event.stopPropagation() event.stopPropagation()
if (!canOpenTabRename(props.dragging, editing(), committing)) return if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return
const session = props.session() const session = props.session()
if (!session) return if (!session) return
titleEl.textContent = session.title titleEl.textContent = session.title
@@ -15,6 +15,7 @@ import { useCommand } from "@/context/command"
import { useTabs } from "@/context/tabs" import { useTabs } from "@/context/tabs"
import { createTabPromptState } from "@/context/prompt" import { createTabPromptState } from "@/context/prompt"
import { base64Encode } from "@opencode-ai/core/util/encode" import { base64Encode } from "@opencode-ai/core/util/encode"
import { showToast } from "@/utils/toast"
import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture" import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture"
function SessionTabSlot(props: { function SessionTabSlot(props: {
@@ -52,6 +53,25 @@ function SessionTabSlot(props: {
const missingSession = createMemo(() => !!props.serverCtx() && !loadedSession.loading && !session()) const missingSession = createMemo(() => !!props.serverCtx() && !loadedSession.loading && !session())
let prefetched = false let prefetched = false
const rename = async (title: string) => {
const value = session()
const ctx = props.serverCtx()
if (!value || !ctx) return
ctx.sync.session.remember({ ...value, title })
try {
await ctx.sdk.api.session.rename({ sessionID: value.id, title })
} catch (err) {
const current = session()
const currentCtx = props.serverCtx()
if (current && currentCtx) currentCtx.sync.session.remember({ ...current, title: value.title })
showToast({
title: language.t("common.requestFailed"),
description: err instanceof Error ? err.message : undefined,
})
}
}
createEffect(() => { createEffect(() => {
const ctx = props.serverCtx() const ctx = props.serverCtx()
const value = session() const value = session()
@@ -99,16 +119,7 @@ function SessionTabSlot(props: {
server={props.tab.server} server={props.tab.server}
session={session} session={session}
fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)} fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)}
onTitleChange={(title) => { onRename={rename}
const value = session()
const ctx = props.serverCtx()
if (value && ctx) ctx.sync.session.remember({ ...value, title })
}}
onTitleChangeFailed={(title) => {
const value = session()
const ctx = props.serverCtx()
if (value && ctx) ctx.sync.session.remember({ ...value, title })
}}
onNavigate={() => props.onNavigate(ref)} onNavigate={() => props.onNavigate(ref)}
onClose={props.onClose} onClose={props.onClose}
active={props.active()} active={props.active()}