From 67a12d61263002c29691ddbdfe2b1e4e10196d9f Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Tue, 9 Dec 2025 20:13:35 +0000 Subject: [PATCH] Add session rename dialogs and API wiring --- .../src/components/instance-welcome-view.tsx | 116 +++++++++++++--- packages/ui/src/components/session-list.tsx | 58 +++++++- .../src/components/session-rename-dialog.tsx | 130 ++++++++++++++++++ packages/ui/src/stores/session-actions.ts | 30 ++++ packages/ui/src/stores/sessions.ts | 2 + 5 files changed, 317 insertions(+), 19 deletions(-) create mode 100644 packages/ui/src/components/session-rename-dialog.tsx diff --git a/packages/ui/src/components/instance-welcome-view.tsx b/packages/ui/src/components/instance-welcome-view.tsx index 214481c3..e8a15548 100644 --- a/packages/ui/src/components/instance-welcome-view.tsx +++ b/packages/ui/src/components/instance-welcome-view.tsx @@ -1,12 +1,14 @@ import { Component, createSignal, Show, For, createEffect, onMount, onCleanup, createMemo } from "solid-js" -import { Loader2, Trash2 } from "lucide-solid" +import { Loader2, Pencil, Trash2 } from "lucide-solid" import type { Instance } from "../types/instance" -import { getParentSessions, createSession, setActiveParentSession, deleteSession, loading } from "../stores/sessions" +import { getParentSessions, createSession, setActiveParentSession, deleteSession, loading, renameSession } from "../stores/sessions" import InstanceInfo from "./instance-info" import Kbd from "./kbd" +import SessionRenameDialog from "./session-rename-dialog" import { keyboardRegistry, type KeyboardShortcut } from "../lib/keyboard-registry" import { isMac } from "../lib/keyboard-utils" +import { showToastNotification } from "../lib/notifications" import { getLogger } from "../lib/logger" const log = getLogger("actions") @@ -24,6 +26,8 @@ const InstanceWelcomeView: Component = (props) => { const [isDesktopLayout, setIsDesktopLayout] = createSignal( typeof window !== "undefined" ? window.matchMedia("(min-width: 1024px)").matches : false, ) + const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null) + const [isRenaming, setIsRenaming] = createSignal(false) const parentSessions = () => getParentSessions(props.instance.id) const isFetchingSessions = createMemo(() => Boolean(loading().fetchingSessions.get(props.instance.id))) @@ -74,6 +78,25 @@ const InstanceWelcomeView: Component = (props) => { } function handleKeyDown(e: KeyboardEvent) { + let activeElement: HTMLElement | null = null + if (typeof document !== "undefined") { + activeElement = document.activeElement as HTMLElement | null + } + const insideModal = activeElement?.closest(".modal-surface") || activeElement?.closest("[role='dialog']") + const isEditingField = + activeElement && + (["INPUT", "TEXTAREA", "SELECT"].includes(activeElement.tagName) || + activeElement.isContentEditable || + Boolean(insideModal)) + + if (isEditingField) { + if (insideModal && e.key === "Escape" && renameTarget()) { + e.preventDefault() + closeRenameDialog() + } + return + } + if (showInstanceInfoOverlay()) { if (e.key === "Escape") { e.preventDefault() @@ -81,53 +104,67 @@ const InstanceWelcomeView: Component = (props) => { } return } - + const sessions = parentSessions() - + if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "n") { e.preventDefault() handleNewSession() return } - + if (sessions.length === 0) return - + + const listFocused = focusMode() === "sessions" + if (e.key === "ArrowDown") { + if (!listFocused) { + setFocusMode("sessions") + setSelectedIndex(0) + } e.preventDefault() const newIndex = Math.min(selectedIndex() + 1, sessions.length - 1) setSelectedIndex(newIndex) - setFocusMode("sessions") scrollToIndex(newIndex) - } else if (e.key === "ArrowUp") { + return + } + + if (e.key === "ArrowUp") { + if (!listFocused) { + setFocusMode("sessions") + setSelectedIndex(Math.max(parentSessions().length - 1, 0)) + } e.preventDefault() const newIndex = Math.max(selectedIndex() - 1, 0) setSelectedIndex(newIndex) - setFocusMode("sessions") scrollToIndex(newIndex) - } else if (e.key === "PageDown") { + return + } + + if (!listFocused) { + return + } + + if (e.key === "PageDown") { e.preventDefault() const pageSize = 5 const newIndex = Math.min(selectedIndex() + pageSize, sessions.length - 1) setSelectedIndex(newIndex) - setFocusMode("sessions") scrollToIndex(newIndex) } else if (e.key === "PageUp") { e.preventDefault() const pageSize = 5 const newIndex = Math.max(selectedIndex() - pageSize, 0) setSelectedIndex(newIndex) - setFocusMode("sessions") scrollToIndex(newIndex) } else if (e.key === "Home") { e.preventDefault() setSelectedIndex(0) - setFocusMode("sessions") scrollToIndex(0) } else if (e.key === "End") { e.preventDefault() const newIndex = sessions.length - 1 setSelectedIndex(newIndex) - setFocusMode("sessions") scrollToIndex(newIndex) } else if (e.key === "Enter") { e.preventDefault() @@ -138,6 +175,7 @@ const InstanceWelcomeView: Component = (props) => { } } + async function handleEnterKey() { const sessions = parentSessions() const index = selectedIndex() @@ -234,6 +272,31 @@ const InstanceWelcomeView: Component = (props) => { } } + function openRenameDialogForSession(sessionId: string, title: string) { + const label = title && title.trim() ? title : sessionId + setRenameTarget({ id: sessionId, title: title ?? "", label }) + } + + function closeRenameDialog() { + setRenameTarget(null) + } + + async function handleRenameSubmit(nextTitle: string) { + const target = renameTarget() + if (!target) return + + setIsRenaming(true) + try { + await renameSession(props.instance.id, target.id, nextTitle) + setRenameTarget(null) + } catch (error) { + log.error("Failed to rename session:", error) + showToastNotification({ message: "Unable to rename session", variant: "error" }) + } finally { + setIsRenaming(false) + } + } + async function handleNewSession() { if (isCreating()) return @@ -355,6 +418,18 @@ const InstanceWelcomeView: Component = (props) => {
↵ +
+ + ) } - - export default InstanceWelcomeView - +export default InstanceWelcomeView diff --git a/packages/ui/src/components/session-list.tsx b/packages/ui/src/components/session-list.tsx index 2e94abd6..98984b28 100644 --- a/packages/ui/src/components/session-list.tsx +++ b/packages/ui/src/components/session-list.tsx @@ -1,13 +1,14 @@ import { Component, For, Show, createSignal, createEffect, onCleanup, onMount, createMemo, JSX } from "solid-js" import type { Session, SessionStatus } from "../types/session" import { getSessionStatus } from "../stores/session-status" -import { MessageSquare, Info, X, Copy, Trash2 } from "lucide-solid" +import { MessageSquare, Info, X, Copy, Trash2, Pencil } from "lucide-solid" import KeyboardHint from "./keyboard-hint" import Kbd from "./kbd" +import SessionRenameDialog from "./session-rename-dialog" import { keyboardRegistry } from "../lib/keyboard-registry" import { formatShortcut } from "../lib/keyboard-utils" import { showToastNotification } from "../lib/notifications" -import { deleteSession, loading } from "../stores/sessions" +import { deleteSession, loading, renameSession } from "../stores/sessions" import { getLogger } from "../lib/logger" const log = getLogger("session") @@ -66,6 +67,8 @@ const SessionList: Component = (props) => { const [isResizing, setIsResizing] = createSignal(false) const [startX, setStartX] = createSignal(0) const [startWidth, setStartWidth] = createSignal(DEFAULT_WIDTH) + const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null) + const [isRenaming, setIsRenaming] = createSignal(false) const infoShortcut = keyboardRegistry.get("switch-to-info") const isSessionDeleting = (sessionId: string) => { @@ -132,8 +135,36 @@ const SessionList: Component = (props) => { showToastNotification({ message: "Unable to delete session", variant: "error" }) } } + + const openRenameDialog = (sessionId: string) => { + const session = props.sessions.get(sessionId) + if (!session) return + const label = session.title && session.title.trim() ? session.title : sessionId + setRenameTarget({ id: sessionId, title: session.title ?? "", label }) + } + + const closeRenameDialog = () => { + setRenameTarget(null) + } + + const handleRenameSubmit = async (nextTitle: string) => { + const target = renameTarget() + if (!target) return + + setIsRenaming(true) + try { + await renameSession(props.instanceId, target.id, nextTitle) + setRenameTarget(null) + } catch (error) { + log.error(`Failed to rename session ${target.id}:`, error) + showToastNotification({ message: "Unable to rename session", variant: "error" }) + } finally { + setIsRenaming(false) + } + } const clampWidth = (width: number) => Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, width)) + @@ -281,6 +312,19 @@ const SessionList: Component = (props) => { > + { + event.stopPropagation() + openRenameDialog(rowProps.sessionId) + }} + role="button" + tabIndex={0} + aria-label="Rename session" + title="Rename session" + > + + handleDeleteSession(event, rowProps.sessionId)} @@ -418,8 +462,18 @@ const SessionList: Component = (props) => { {props.footerContent ?? null}
+ + ) } export default SessionList + diff --git a/packages/ui/src/components/session-rename-dialog.tsx b/packages/ui/src/components/session-rename-dialog.tsx new file mode 100644 index 00000000..ce28c21f --- /dev/null +++ b/packages/ui/src/components/session-rename-dialog.tsx @@ -0,0 +1,130 @@ +import { Dialog } from "@kobalte/core/dialog" +import { Component, Show, createEffect, createSignal } from "solid-js" + +interface SessionRenameDialogProps { + open: boolean + currentTitle: string + sessionLabel?: string + isSubmitting?: boolean + onRename: (nextTitle: string) => Promise | void + onClose: () => void +} + +const SessionRenameDialog: Component = (props) => { + const [title, setTitle] = createSignal("") + const inputId = `session-rename-${Math.random().toString(36).slice(2)}` + let inputRef: HTMLInputElement | undefined + + createEffect(() => { + if (!props.open) return + setTitle(props.currentTitle ?? "") + }) + + createEffect(() => { + if (!props.open) return + if (typeof window === "undefined" || typeof window.requestAnimationFrame !== "function") return + window.requestAnimationFrame(() => { + inputRef?.focus() + inputRef?.select() + }) + }) + + const isSubmitting = () => Boolean(props.isSubmitting) + const isRenameDisabled = () => isSubmitting() || !title().trim() + + async function handleRename(event?: Event) { + event?.preventDefault() + if (isRenameDisabled()) return + await props.onRename(title().trim()) + } + + const description = () => { + if (props.sessionLabel && props.sessionLabel.trim()) { + return `Update the title for "${props.sessionLabel}".` + } + return "Set a new title for this session." + } + + return ( + { + if (!open && !isSubmitting()) { + props.onClose() + } + }} + > + + +
+ + Rename Session + + {description()} + + +
+
+ + { + inputRef = element + }} + type="text" + value={title()} + onInput={(event) => setTitle(event.currentTarget.value)} + placeholder="Enter a session name" + class="w-full px-3 py-2 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent" + /> +
+ +
+ + +
+
+
+
+
+
+ ) +} + +export default SessionRenameDialog diff --git a/packages/ui/src/stores/session-actions.ts b/packages/ui/src/stores/session-actions.ts index e0ac5c49..667f3557 100644 --- a/packages/ui/src/stores/session-actions.ts +++ b/packages/ui/src/stores/session-actions.ts @@ -334,9 +334,39 @@ async function updateSessionModel( updateSessionInfo(instanceId, sessionId) } +async function renameSession(instanceId: string, sessionId: string, nextTitle: string): Promise { + const instance = instances().get(instanceId) + if (!instance || !instance.client) { + throw new Error("Instance not ready") + } + + const session = sessions().get(instanceId)?.get(sessionId) + if (!session) { + throw new Error("Session not found") + } + + const trimmedTitle = nextTitle.trim() + if (!trimmedTitle) { + throw new Error("Session title is required") + } + + await instance.client.session.update({ + path: { id: sessionId }, + body: { title: trimmedTitle }, + }) + + withSession(instanceId, sessionId, (current) => { + current.title = trimmedTitle + const time = { ...(current.time ?? {}) } + time.updated = Date.now() + current.time = time + }) +} + export { abortSession, executeCustomCommand, + renameSession, runShellCommand, sendMessage, updateSessionAgent, diff --git a/packages/ui/src/stores/sessions.ts b/packages/ui/src/stores/sessions.ts index 49b7787f..2ab6f2bd 100644 --- a/packages/ui/src/stores/sessions.ts +++ b/packages/ui/src/stores/sessions.ts @@ -41,6 +41,7 @@ import { import { abortSession, executeCustomCommand, + renameSession, runShellCommand, sendMessage, updateSessionAgent, @@ -82,6 +83,7 @@ export { createSession, deleteSession, executeCustomCommand, + renameSession, runShellCommand, fetchAgents, fetchProviders,