import { Component, createSignal, Show, For, createEffect, onMount, onCleanup, createMemo } from "solid-js" import { Loader2, Pencil, Trash2 } from "lucide-solid" import type { Instance } from "../types/instance" 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") interface InstanceWelcomeViewProps { instance: Instance } const InstanceWelcomeView: Component = (props) => { const [isCreating, setIsCreating] = createSignal(false) const [selectedIndex, setSelectedIndex] = createSignal(0) const [focusMode, setFocusMode] = createSignal<"sessions" | "new-session" | null>("sessions") const [showInstanceInfoOverlay, setShowInstanceInfoOverlay] = createSignal(false) 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))) const isSessionDeleting = (sessionId: string) => { const deleting = loading().deletingSession.get(props.instance.id) return deleting ? deleting.has(sessionId) : false } const newSessionShortcut = createMemo(() => { const registered = keyboardRegistry.get("session-new") if (registered) return registered return { id: "session-new-display", key: "n", modifiers: { shift: true, meta: isMac(), ctrl: !isMac(), }, handler: () => {}, description: "New Session", context: "global", } }) const newSessionShortcutString = createMemo(() => (isMac() ? "cmd+shift+n" : "ctrl+shift+n")) createEffect(() => { const sessions = parentSessions() if (sessions.length === 0) { setFocusMode("new-session") setSelectedIndex(0) } else { setFocusMode("sessions") setSelectedIndex(0) } }) const openInstanceInfoOverlay = () => { if (isDesktopLayout()) return setShowInstanceInfoOverlay(true) } const closeInstanceInfoOverlay = () => setShowInstanceInfoOverlay(false) function scrollToIndex(index: number) { const element = document.querySelector(`[data-session-index="${index}"]`) if (element) { element.scrollIntoView({ block: "nearest", behavior: "auto" }) } } 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() closeInstanceInfoOverlay() } 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) scrollToIndex(newIndex) 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) scrollToIndex(newIndex) return } if (!listFocused) { return } if (e.key === "PageDown") { e.preventDefault() const pageSize = 5 const newIndex = Math.min(selectedIndex() + pageSize, sessions.length - 1) setSelectedIndex(newIndex) scrollToIndex(newIndex) } else if (e.key === "PageUp") { e.preventDefault() const pageSize = 5 const newIndex = Math.max(selectedIndex() - pageSize, 0) setSelectedIndex(newIndex) scrollToIndex(newIndex) } else if (e.key === "Home") { e.preventDefault() setSelectedIndex(0) scrollToIndex(0) } else if (e.key === "End") { e.preventDefault() const newIndex = sessions.length - 1 setSelectedIndex(newIndex) scrollToIndex(newIndex) } else if (e.key === "Enter") { e.preventDefault() void handleEnterKey() } else if (e.key === "Delete" || e.key === "Backspace") { e.preventDefault() void handleDeleteKey() } } async function handleEnterKey() { const sessions = parentSessions() const index = selectedIndex() if (index < sessions.length) { await handleSessionSelect(sessions[index].id) } } async function handleDeleteKey() { const sessions = parentSessions() const index = selectedIndex() if (index >= sessions.length) { return } await handleSessionDelete(sessions[index].id) const updatedSessions = parentSessions() if (updatedSessions.length === 0) { setFocusMode("new-session") setSelectedIndex(0) return } const nextIndex = Math.min(index, updatedSessions.length - 1) setSelectedIndex(nextIndex) setFocusMode("sessions") scrollToIndex(nextIndex) } onMount(() => { window.addEventListener("keydown", handleKeyDown) onCleanup(() => { window.removeEventListener("keydown", handleKeyDown) }) }) onMount(() => { const mediaQuery = window.matchMedia("(min-width: 1024px)") const handleMediaChange = (matches: boolean) => { setIsDesktopLayout(matches) if (matches) { closeInstanceInfoOverlay() } } const listener = (event: MediaQueryListEvent) => handleMediaChange(event.matches) if (typeof mediaQuery.addEventListener === "function") { mediaQuery.addEventListener("change", listener) onCleanup(() => { mediaQuery.removeEventListener("change", listener) }) } else { mediaQuery.addListener(listener) onCleanup(() => { mediaQuery.removeListener(listener) }) } handleMediaChange(mediaQuery.matches) }) function formatRelativeTime(timestamp: number): string { const seconds = Math.floor((Date.now() - timestamp) / 1000) const minutes = Math.floor(seconds / 60) const hours = Math.floor(minutes / 60) const days = Math.floor(hours / 24) if (days > 0) return `${days}d ago` if (hours > 0) return `${hours}h ago` if (minutes > 0) return `${minutes}m ago` return "just now" } function formatTimestamp(timestamp: number): string { return new Date(timestamp).toLocaleString() } async function handleSessionSelect(sessionId: string) { setActiveParentSession(props.instance.id, sessionId) } async function handleSessionDelete(sessionId: string) { if (isSessionDeleting(sessionId)) return try { await deleteSession(props.instance.id, sessionId) } catch (error) { log.error("Failed to delete session:", error) } } 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 setIsCreating(true) try { const session = await createSession(props.instance.id) setActiveParentSession(props.instance.id, session.id) } catch (error) { log.error("Failed to create session:", error) } finally { setIsCreating(false) } } return (
0} fallback={

No Previous Sessions

Create a new session below to get started

} >

Loading Sessions

Fetching your previous sessions...

} >

Resume Session

{parentSessions().length} {parentSessions().length === 1 ? "session" : "sessions"} available

{(session, index) => { const isFocused = () => focusMode() === "sessions" && selectedIndex() === index() return (
) }}

Start New Session

We’ll reuse your last agent/model automatically

event.stopPropagation()} >
) } export default InstanceWelcomeView