diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 46c9c9154..c565d197f 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -1000,69 +1000,6 @@ export default function Layout(props: ParentProps) { } } - async function deleteSession(session: Session) { - const [store, setStore] = globalSync.child(session.directory) - const sessions = (store.session ?? []).filter((s) => !s.parentID && !s.time?.archived) - const index = sessions.findIndex((s) => s.id === session.id) - const nextSession = sessions[index + 1] ?? sessions[index - 1] - - const result = await globalSDK.client.session - .delete({ directory: session.directory, sessionID: session.id }) - .then((x) => x.data) - .catch((err) => { - showToast({ - title: language.t("session.delete.failed.title"), - description: errorMessage(err), - }) - return false - }) - - if (!result) return - - setStore( - produce((draft) => { - const removed = new Set([session.id]) - - const byParent = new Map() - for (const item of draft.session) { - const parentID = item.parentID - if (!parentID) continue - const existing = byParent.get(parentID) - if (existing) { - existing.push(item.id) - continue - } - byParent.set(parentID, [item.id]) - } - - const stack = [session.id] - while (stack.length) { - const parentID = stack.pop() - if (!parentID) continue - - const children = byParent.get(parentID) - if (!children) continue - - for (const child of children) { - if (removed.has(child)) continue - removed.add(child) - stack.push(child) - } - } - - draft.session = draft.session.filter((s) => !removed.has(s.id)) - }), - ) - - if (session.id === params.id) { - if (nextSession) { - navigate(`/${params.dir}/session/${nextSession.id}`) - } else { - navigate(`/${params.dir}/session`) - } - } - } - command.register(() => { const commands: CommandOption[] = [ { @@ -1316,15 +1253,6 @@ export default function Layout(props: ParentProps) { globalSync.project.meta(project.worktree, { name }) } - async function renameSession(session: Session, next: string) { - if (next === session.title) return - await globalSDK.client.session.update({ - directory: session.directory, - sessionID: session.id, - title: next, - }) - } - const renameWorkspace = (directory: string, next: string, projectId?: string, branch?: string) => { const current = workspaceName(directory, projectId, branch) ?? branch ?? getFilename(directory) if (current === next) return @@ -1475,33 +1403,6 @@ export default function Layout(props: ParentProps) { }) } - function DialogDeleteSession(props: { session: Session }) { - const handleDelete = async () => { - await deleteSession(props.session) - dialog.close() - } - - return ( - -
-
- - {language.t("session.delete.confirm", { name: props.session.title })} - -
-
- - -
-
-
- ) - } - function DialogDeleteWorkspace(props: { root: string; directory: string }) { const name = createMemo(() => getFilename(props.directory)) const [data, setData] = createStore({ @@ -1855,10 +1756,6 @@ export default function Layout(props: ParentProps) { const hoverAllowed = createMemo(() => !props.mobile && sidebarExpanded()) const hoverEnabled = createMemo(() => (props.popover ?? true) && hoverAllowed()) const isActive = createMemo(() => props.session.id === params.id) - const [menu, setMenu] = createStore({ - open: false, - pendingRename: false, - }) const hoverPrefetch = { current: undefined as ReturnType | undefined } const cancelHoverPrefetch = () => { @@ -1885,7 +1782,7 @@ export default function Layout(props: ParentProps) { const item = ( - props.session.title} - onSave={(next) => renameSession(props.session, next)} - class="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - displayClass="text-14-regular text-text-strong grow-1 min-w-0 overflow-hidden text-ellipsis truncate" - stopPropagation - /> + + {props.session.title} + {(summary) => (
@@ -1989,49 +1881,25 @@ export default function Layout(props: ParentProps) {
- setMenu("open", open)}> - - - - - { - if (!menu.pendingRename) return - event.preventDefault() - setMenu("pendingRename", false) - openEditor(`session:${props.session.id}`, props.session.title) - }} - > - { - setMenu("pendingRename", true) - setMenu("open", false) - }} - > - {language.t("common.rename")} - - archiveSession(props.session)}> - {language.t("common.archive")} - - - dialog.show(() => )}> - {language.t("common.delete")} - - - - + + { + event.preventDefault() + event.stopPropagation() + void archiveSession(props.session) + }} + /> +
) diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 644fa66b3..2143cd34b 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -25,7 +25,7 @@ import { Icon } from "@opencode-ai/ui/icon" import { Tooltip, TooltipKeybind } from "@opencode-ai/ui/tooltip" import { DropdownMenu } from "@opencode-ai/ui/dropdown-menu" import { Dialog } from "@opencode-ai/ui/dialog" -import { TextField } from "@opencode-ai/ui/text-field" +import { InlineInput } from "@opencode-ai/ui/inline-input" import { ResizeHandle } from "@opencode-ai/ui/resize-handle" import { Tabs } from "@opencode-ai/ui/tabs" import { useCodeComponent } from "@opencode-ai/ui/context/code" @@ -440,6 +440,15 @@ export default function Page() { return sync.session.history.loading(id) }) + const [title, setTitle] = createStore({ + draft: "", + editing: false, + saving: false, + menuOpen: false, + pendingRename: false, + }) + let titleRef: HTMLInputElement | undefined + const errorMessage = (err: unknown) => { if (err && typeof err === "object" && "data" in err) { const data = (err as { data?: { message?: string } }).data @@ -449,6 +458,60 @@ export default function Page() { return language.t("common.requestFailed") } + createEffect( + on( + () => params.id, + () => setTitle({ draft: "", editing: false, saving: false, menuOpen: false, pendingRename: false }), + { defer: true }, + ), + ) + + const openTitleEditor = () => { + if (!params.id) return + setTitle({ editing: true, draft: info()?.title ?? "" }) + requestAnimationFrame(() => { + titleRef?.focus() + titleRef?.select() + }) + } + + const closeTitleEditor = () => { + if (title.saving) return + setTitle({ editing: false, saving: false }) + } + + const saveTitleEditor = async () => { + const sessionID = params.id + if (!sessionID) return + if (title.saving) return + + const next = title.draft.trim() + if (!next || next === (info()?.title ?? "")) { + setTitle({ editing: false, saving: false }) + return + } + + setTitle("saving", true) + await sdk.client.session + .update({ sessionID, title: next }) + .then(() => { + sync.set( + produce((draft) => { + const index = draft.session.findIndex((s) => s.id === sessionID) + if (index !== -1) draft.session[index].title = next + }), + ) + setTitle({ editing: false, saving: false }) + }) + .catch((err) => { + setTitle("saving", false) + showToast({ + title: language.t("common.requestFailed"), + description: errorMessage(err), + }) + }) + } + async function archiveSession(sessionID: string) { const session = sync.session.get(sessionID) if (!session) return @@ -555,74 +618,6 @@ export default function Page() { return true } - function DialogRenameSession(props: { sessionID: string }) { - const [data, setData] = createStore({ - title: sync.session.get(props.sessionID)?.title ?? "", - saving: false, - }) - - const submit = (event: Event) => { - event.preventDefault() - if (data.saving) return - - const title = data.title.trim() - if (!title) { - dialog.close() - return - } - - const current = sync.session.get(props.sessionID)?.title ?? "" - if (title === current) { - dialog.close() - return - } - - setData("saving", true) - void sdk.client.session - .update({ sessionID: props.sessionID, title }) - .then(() => { - sync.set( - produce((draft) => { - const index = draft.session.findIndex((s) => s.id === props.sessionID) - if (index !== -1) draft.session[index].title = title - }), - ) - dialog.close() - }) - .catch((err) => { - showToast({ - title: language.t("common.requestFailed"), - description: errorMessage(err), - }) - }) - .finally(() => { - setData("saving", false) - }) - } - - return ( - -
- setData("title", value)} - /> -
- - -
- -
- ) - } - function DialogDeleteSession(props: { sessionID: string }) { const title = createMemo(() => sync.session.get(props.sessionID)?.title ?? language.t("command.session.new")) const handleDelete = async () => { @@ -2208,7 +2203,7 @@ export default function Page() { }} >
-
+
- -

{info()?.title}

+ + + {info()?.title} + + } + > + { + titleRef = el + }} + value={title.draft} + disabled={title.saving} + class="text-16-medium text-text-strong grow-1 min-w-0" + onInput={(event) => setTitle("draft", event.currentTarget.value)} + onKeyDown={(event) => { + event.stopPropagation() + if (event.key === "Enter") { + event.preventDefault() + void saveTitleEditor() + return + } + if (event.key === "Escape") { + event.preventDefault() + closeTitleEditor() + } + }} + onBlur={() => closeTitleEditor()} + /> +
{(id) => (
- + setTitle("menuOpen", open)} + > - + { + if (!title.pendingRename) return + event.preventDefault() + setTitle("pendingRename", false) + openTitleEditor() + }} + > dialog.show(() => )} + onSelect={() => { + setTitle({ pendingRename: true, menuOpen: false }) + }} > {language.t("common.rename")}