From e1c41941a9c15a144a916108ff7aeb7b79155de6 Mon Sep 17 00:00:00 2001 From: Brendan Allan <14191578+Brendonovich@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:54:24 +0800 Subject: [PATCH] refactor(app): extract tab rename adapter (#39079) --- .../src/components/titlebar-tab-gesture.ts | 4 +-- .../app/src/components/titlebar-tab-nav.tsx | 35 ++++--------------- .../app/src/components/titlebar-tab-strip.tsx | 31 ++++++++++------ 3 files changed, 29 insertions(+), 41 deletions(-) diff --git a/packages/app/src/components/titlebar-tab-gesture.ts b/packages/app/src/components/titlebar-tab-gesture.ts index 329bf5e864..0aa5118936 100644 --- a/packages/app/src/components/titlebar-tab-gesture.ts +++ b/packages/app/src/components/titlebar-tab-gesture.ts @@ -12,6 +12,6 @@ export function forwardTabRef(ref: Ref | undefined, element: HTM if (typeof ref === "function") ref(element) } -export function canOpenTabRename(dragging: boolean | undefined, editing: boolean, committing: boolean) { - return !dragging && !editing && !committing +export function canOpenTabRename(dragging: boolean | undefined, editing: boolean, pending: boolean) { + return !dragging && !editing && !pending } diff --git a/packages/app/src/components/titlebar-tab-nav.tsx b/packages/app/src/components/titlebar-tab-nav.tsx index 3058e6881a..ceb43d4486 100644 --- a/packages/app/src/components/titlebar-tab-nav.tsx +++ b/packages/app/src/components/titlebar-tab-nav.tsx @@ -1,14 +1,13 @@ import { createEffect, createMemo, createSignal, onCleanup, Show, type Ref } from "solid-js" import { makeEventListener } from "@solid-primitives/event-listener" import { createResizeObserver } from "@solid-primitives/resize-observer" +import { createMutation } from "@tanstack/solid-query" import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2" import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon" import { useGlobal } from "@/context/global" -import { useLanguage } from "@/context/language" import { ServerConnection, serverName } from "@/context/server" import { displayName, projectForSession } from "@/pages/layout/helpers" import { SessionTabAvatar } from "@/pages/layout/session-tab-avatar" -import { showToast } from "@/utils/toast" import type { Session } from "@opencode-ai/sdk/v2" import { canOpenTabRename, forwardTabRef } from "./titlebar-tab-gesture" import { TabPreviewPopover } from "./titlebar-tab-popover" @@ -23,8 +22,7 @@ export function TabNavItem(props: { server: ServerConnection.Key session: () => Session | undefined fallbackTitle?: string - onTitleChange?: (title: string) => void - onTitleChangeFailed?: (title: string) => void + onRename: (title: string) => Promise onClose: () => void onNavigate: () => void active?: boolean @@ -34,13 +32,12 @@ export function TabNavItem(props: { pressed?: boolean hidden?: boolean }) { - const language = useLanguage() const [editing, setEditing] = createSignal(false) const [titleOverflowing, setTitleOverflowing] = createSignal(false) let tabRoot!: HTMLDivElement let titleEl!: HTMLSpanElement - let committing = false let measureFrame: number | undefined + const rename = createMutation(() => ({ mutationFn: props.onRename })) const closeTab = (event: MouseEvent) => { event.preventDefault() @@ -116,40 +113,20 @@ export function TabNavItem(props: { 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) => { - if (committing || !editing()) return - committing = true + if (rename.isPending || !editing()) return const original = props.session()?.title ?? "" const next = (titleEl.textContent ?? "").trim() titleEl.scrollLeft = 0 - if (save && next && next !== original) props.onTitleChange?.(next) setEditing(false) if (!save || !next || next === original) { - committing = false return } - try { - await rename(next) - } catch (err) { - props.onTitleChangeFailed?.(original) - showToast({ - title: language.t("common.requestFailed"), - description: err instanceof Error ? err.message : undefined, - }) - } - - committing = false + await rename.mutateAsync(next) } createEffect(() => { @@ -163,7 +140,7 @@ export function TabNavItem(props: { const openRename = (event: MouseEvent) => { event.preventDefault() event.stopPropagation() - if (!canOpenTabRename(props.dragging, editing(), committing)) return + if (!canOpenTabRename(props.dragging, editing(), rename.isPending)) return const session = props.session() if (!session) return titleEl.textContent = session.title diff --git a/packages/app/src/components/titlebar-tab-strip.tsx b/packages/app/src/components/titlebar-tab-strip.tsx index 7ef53324cd..9dc44a65cd 100644 --- a/packages/app/src/components/titlebar-tab-strip.tsx +++ b/packages/app/src/components/titlebar-tab-strip.tsx @@ -15,6 +15,7 @@ import { useCommand } from "@/context/command" import { useTabs } from "@/context/tabs" import { createTabPromptState } from "@/context/prompt" import { base64Encode } from "@opencode-ai/core/util/encode" +import { showToast } from "@/utils/toast" import { canStartTabDrag, isTabCloseTarget } from "./titlebar-tab-gesture" function SessionTabSlot(props: { @@ -52,6 +53,25 @@ function SessionTabSlot(props: { const missingSession = createMemo(() => !!props.serverCtx() && !loadedSession.loading && !session()) 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(() => { const ctx = props.serverCtx() const value = session() @@ -99,16 +119,7 @@ function SessionTabSlot(props: { server={props.tab.server} session={session} fallbackTitle={persisted()?.title ?? (missingSession() ? language.t("session.tab.unknown") : undefined)} - onTitleChange={(title) => { - 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 }) - }} + onRename={rename} onNavigate={() => props.onNavigate(ref)} onClose={props.onClose} active={props.active()}