浏览代码

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

Brendan Allan 3 周之前
父节点
当前提交
e1c41941a9

+ 2 - 2
packages/app/src/components/titlebar-tab-gesture.ts

@@ -12,6 +12,6 @@ export function forwardTabRef(ref: Ref<HTMLDivElement> | 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
 }

+ 6 - 29
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<void>
   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

+ 21 - 10
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()}