refactor(app): extract tab rename adapter (#39079)
This commit is contained in:
@@ -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()}
|
||||||
|
|||||||
Reference in New Issue
Block a user