From 82460b7a00add65e91a816770c4b960c0c187119 Mon Sep 17 00:00:00 2001 From: OmarMcAdam Date: Thu, 22 Jan 2026 17:42:43 -0700 Subject: [PATCH] feat: add session rename command --- packages/app/src/app/app.tsx | 1 - .../app/components/rename-session-modal.tsx | 64 ++++++++++++ packages/app/src/app/pages/session.tsx | 98 ++++++++++++------- packages/app/src/i18n/locales/en.ts | 4 + packages/app/src/i18n/locales/zh.ts | 4 + 5 files changed, 134 insertions(+), 37 deletions(-) create mode 100644 packages/app/src/app/components/rename-session-modal.tsx diff --git a/packages/app/src/app/app.tsx b/packages/app/src/app/app.tsx index 7ac2ac39d..d22a10cfb 100644 --- a/packages/app/src/app/app.tsx +++ b/packages/app/src/app/app.tsx @@ -464,7 +464,6 @@ export default function App() { } - async function respondPermissionAndRemember( requestID: string, reply: "once" | "always" | "reject" diff --git a/packages/app/src/app/components/rename-session-modal.tsx b/packages/app/src/app/components/rename-session-modal.tsx new file mode 100644 index 000000000..5962119aa --- /dev/null +++ b/packages/app/src/app/components/rename-session-modal.tsx @@ -0,0 +1,64 @@ +import { Show } from "solid-js"; +import { X } from "lucide-solid"; +import { t, currentLocale } from "../../i18n"; + +import Button from "./button"; +import TextInput from "./text-input"; + +export type RenameSessionModalProps = { + open: boolean; + title: string; + busy: boolean; + canSave: boolean; + onClose: () => void; + onSave: () => void; + onTitleChange: (value: string) => void; +}; + +export default function RenameSessionModal(props: RenameSessionModalProps) { + const translate = (key: string) => t(key, currentLocale()); + + return ( + +
+
+
+
+
+

{translate("session.rename_title")}

+

{translate("session.rename_description")}

+
+ +
+ +
+ props.onTitleChange(e.currentTarget.value)} + placeholder={translate("session.rename_placeholder")} + class="bg-gray-3" + onKeyDown={(event) => { + if (event.key !== "Enter") return; + event.preventDefault(); + if (props.canSave) props.onSave(); + }} + /> +
+ +
+ + +
+
+
+
+
+ ); +} diff --git a/packages/app/src/app/pages/session.tsx b/packages/app/src/app/pages/session.tsx index 6105cd8bc..cace6ccf3 100644 --- a/packages/app/src/app/pages/session.tsx +++ b/packages/app/src/app/pages/session.tsx @@ -27,6 +27,7 @@ import { import Button from "../components/button"; import PartView from "../components/part-view"; +import RenameSessionModal from "../components/rename-session-modal"; import WorkspaceChip from "../components/workspace-chip"; import ProviderAuthModal from "../components/provider-auth-modal"; import { isTauriRuntime, isWindowsPlatform } from "../utils"; @@ -82,7 +83,7 @@ export type SessionViewProps = { safeStringify: (value: unknown) => string; error: string | null; sessionStatus: string; - renameSession: (sessionId: string, title: string) => Promise | void; + renameSession: (sessionId: string, title: string) => Promise; openConnect: () => void; startProviderAuth: (providerId?: string) => Promise; openProviderAuthModal: () => Promise; @@ -122,6 +123,9 @@ export default function SessionView(props: SessionViewProps) { const [commandToast, setCommandToast] = createSignal(null); const [commandIndex, setCommandIndex] = createSignal(0); const [providerAuthActionBusy, setProviderAuthActionBusy] = createSignal(false); + const [renameModalOpen, setRenameModalOpen] = createSignal(false); + const [renameTitle, setRenameTitle] = createSignal(""); + const [renameBusy, setRenameBusy] = createSignal(false); let promptInputEl: HTMLTextAreaElement | undefined; @@ -315,6 +319,50 @@ export default function SessionView(props: SessionViewProps) { return !hasAssistantTextAfterLastUser(); }); + const selectedSessionTitle = createMemo(() => { + const id = props.selectedSessionId; + if (!id) return ""; + return props.sessions.find((session) => session.id === id)?.title ?? ""; + }); + + const renameCanSave = createMemo(() => { + if (renameBusy()) return false; + const next = renameTitle().trim(); + if (!next) return false; + return next !== selectedSessionTitle().trim(); + }); + + const openRenameModal = () => { + if (!props.selectedSessionId) { + setCommandToast("No session selected"); + return; + } + setRenameTitle(selectedSessionTitle()); + setRenameModalOpen(true); + }; + + const closeRenameModal = () => { + if (renameBusy()) return; + setRenameModalOpen(false); + }; + + const submitRename = async () => { + const sessionId = props.selectedSessionId; + if (!sessionId) return; + const next = renameTitle().trim(); + if (!next || !renameCanSave()) return; + setRenameBusy(true); + try { + await props.renameSession(sessionId, next); + setRenameModalOpen(false); + } catch (error) { + const message = error instanceof Error ? error.message : props.safeStringify(error); + setCommandToast(message); + } finally { + setRenameBusy(false); + } + }; + const clearPrompt = () => { props.setPrompt(""); queueMicrotask(syncPromptHeight); @@ -367,13 +415,6 @@ export default function SessionView(props: SessionViewProps) { return items.length > 4 ? `${preview}, ...` : preview; }; - const activeSessionTitle = createMemo(() => { - const selectedId = props.selectedSessionId; - if (!selectedId) return ""; - const entry = props.sessions.find((session) => session.id === selectedId); - return entry?.title ?? ""; - }); - const handleProviderAuthSelect = async (providerId: string) => { if (providerAuthActionBusy()) return; setProviderAuthActionBusy(true); @@ -521,34 +562,9 @@ export default function SessionView(props: SessionViewProps) { id: "rename", label: "Rename", description: "Rename this session", - run: async () => { - const sessionId = requireSessionId(); - if (!sessionId) return; - - let nextTitle = extractCommandArgs(props.prompt); - if (!nextTitle) { - const fallback = activeSessionTitle(); - const prompted = window.prompt("Rename session", fallback); - if (prompted == null) { - return; - } - nextTitle = prompted.trim(); - } - - if (!nextTitle) { - setCommandToast("Session name is required"); - clearPrompt(); - return; - } - - try { - await props.renameSession(sessionId, nextTitle); - setCommandToast("Session renamed"); - clearPrompt(); - } catch (error) { - const message = error instanceof Error ? error.message : "Rename failed"; - setCommandToast(message); - } + run: () => { + openRenameModal(); + clearPrompt(); }, }, { @@ -1259,6 +1275,16 @@ export default function SessionView(props: SessionViewProps) { onClose={props.closeProviderAuthModal} /> + +
diff --git a/packages/app/src/i18n/locales/en.ts b/packages/app/src/i18n/locales/en.ts index 14a8e580f..a14eb3309 100644 --- a/packages/app/src/i18n/locales/en.ts +++ b/packages/app/src/i18n/locales/en.ts @@ -143,6 +143,10 @@ export default { "session.recents_label": "Recents", "session.model_standard": "Standard", "session.run_button_title": "Run", + "session.rename_title": "Rename session", + "session.rename_description": "Update the name for this session.", + "session.rename_label": "Session name", + "session.rename_placeholder": "Enter a new name", // ==================== Templates ==================== "templates.new": "New", "templates.empty_state": "Starter templates will appear here. Create one or save from a session.", diff --git a/packages/app/src/i18n/locales/zh.ts b/packages/app/src/i18n/locales/zh.ts index 852bf23a1..0e1c0c4ad 100644 --- a/packages/app/src/i18n/locales/zh.ts +++ b/packages/app/src/i18n/locales/zh.ts @@ -143,6 +143,10 @@ export default { "session.recents_label": "最近", "session.model_standard": "标准", "session.run_button_title": "运行", + "session.rename_title": "重命名会话", + "session.rename_description": "更新此会话名称。", + "session.rename_label": "会话名称", + "session.rename_placeholder": "输入新的名称", // ==================== Templates ==================== "templates.new": "新建",