Files
CodeNomad/src/components/instance-welcome-view.tsx
2025-11-07 21:22:46 +00:00

314 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Component, createSignal, Show, For, createEffect, onMount, onCleanup, createMemo } from "solid-js"
import type { Instance } from "../types/instance"
import { getParentSessions, createSession, setActiveParentSession } from "../stores/sessions"
import InstanceInfo from "./instance-info"
import KeyboardHint from "./keyboard-hint"
import Kbd from "./kbd"
import { keyboardRegistry, type KeyboardShortcut } from "../lib/keyboard-registry"
import { isMac } from "../lib/keyboard-utils"
interface InstanceWelcomeViewProps {
instance: Instance
}
const InstanceWelcomeView: Component<InstanceWelcomeViewProps> = (props) => {
const [isCreating, setIsCreating] = createSignal(false)
const [selectedIndex, setSelectedIndex] = createSignal(0)
const [focusMode, setFocusMode] = createSignal<"sessions" | "new-session" | null>("sessions")
const parentSessions = () => getParentSessions(props.instance.id)
const newSessionShortcut = createMemo<KeyboardShortcut>(() => {
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)
}
})
function scrollToIndex(index: number) {
const element = document.querySelector(`[data-session-index="${index}"]`)
if (element) {
element.scrollIntoView({ block: "nearest", behavior: "auto" })
}
}
function handleKeyDown(e: KeyboardEvent) {
const sessions = parentSessions()
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key.toLowerCase() === "n") {
e.preventDefault()
handleNewSession()
return
}
if (sessions.length === 0) return
if (e.key === "ArrowDown") {
e.preventDefault()
const newIndex = Math.min(selectedIndex() + 1, sessions.length - 1)
setSelectedIndex(newIndex)
setFocusMode("sessions")
scrollToIndex(newIndex)
} else if (e.key === "ArrowUp") {
e.preventDefault()
const newIndex = Math.max(selectedIndex() - 1, 0)
setSelectedIndex(newIndex)
setFocusMode("sessions")
scrollToIndex(newIndex)
} else 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()
handleEnterKey()
}
}
async function handleEnterKey() {
const sessions = parentSessions()
const index = selectedIndex()
if (index < sessions.length) {
await handleSessionSelect(sessions[index].id)
}
}
onMount(() => {
window.addEventListener("keydown", handleKeyDown)
onCleanup(() => {
window.removeEventListener("keydown", handleKeyDown)
})
})
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 handleNewSession() {
if (isCreating()) return
setIsCreating(true)
try {
const session = await createSession(props.instance.id)
setActiveParentSession(props.instance.id, session.id)
} catch (error) {
console.error("Failed to create session:", error)
} finally {
setIsCreating(false)
}
}
return (
<div class="flex-1 flex flex-col overflow-hidden bg-surface-secondary">
<div class="flex-1 flex flex-col lg:flex-row gap-4 p-4 overflow-auto">
<div class="flex-1 flex flex-col gap-4 min-h-0">
<Show
when={parentSessions().length > 0}
fallback={
<div class="panel panel-empty-state flex-1 flex flex-col justify-center">
<div class="panel-empty-state-icon">
<svg class="w-12 h-12 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"
/>
</svg>
</div>
<p class="panel-empty-state-title">No Previous Sessions</p>
<p class="panel-empty-state-description">Create a new session below to get started</p>
</div>
}
>
<div class="panel flex flex-col flex-1 min-h-0">
<div class="panel-header">
<h2 class="panel-title">Resume Session</h2>
<p class="panel-subtitle">
{parentSessions().length} {parentSessions().length === 1 ? "session" : "sessions"} available
</p>
</div>
<div class="panel-list panel-list--fill flex-1 min-h-0 overflow-auto">
<For each={parentSessions()}>
{(session, index) => (
<div
class="panel-list-item"
classList={{
"panel-list-item-highlight": focusMode() === "sessions" && selectedIndex() === index(),
}}
>
<button
data-session-index={index()}
class="panel-list-item-content group w-full"
onClick={() => handleSessionSelect(session.id)}
onMouseEnter={() => {
setFocusMode("sessions")
setSelectedIndex(index())
}}
>
<div class="flex items-center justify-between gap-3 w-full">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<span
class="text-sm font-medium text-primary truncate transition-colors"
classList={{
"text-accent":
focusMode() === "sessions" && selectedIndex() === index(),
}}
>
{session.title || "Untitled Session"}
</span>
</div>
<div class="flex items-center gap-3 text-xs text-muted mt-0.5">
<span>{session.agent}</span>
<span></span>
<span>{formatRelativeTime(session.time.updated)}</span>
</div>
</div>
<Show when={focusMode() === "sessions" && selectedIndex() === index()}>
<kbd class="kbd flex-shrink-0"></kbd>
</Show>
</div>
</button>
</div>
)}
</For>
</div>
</div>
</Show>
<div class="panel flex-shrink-0">
<div class="panel-header">
<h2 class="panel-title">Start New Session</h2>
<p class="panel-subtitle">Well reuse your last agent/model automatically</p>
</div>
<div class="panel-body">
<div class="space-y-3">
<button
type="button"
class="button-primary w-full flex items-center justify-center text-sm disabled:cursor-not-allowed"
onClick={handleNewSession}
disabled={isCreating()}
>
<div class="flex items-center gap-2">
{isCreating() ? (
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
) : (
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
)}
<span>Create Session</span>
</div>
<Kbd shortcut={newSessionShortcutString()} class="ml-2" />
</button>
</div>
</div>
</div>
</div>
<div class="lg:w-80 flex-shrink-0">
<div class="sticky top-0">
<InstanceInfo instance={props.instance} />
</div>
</div>
</div>
<div class="panel-footer">
<div class="panel-footer-hints">
<div class="flex items-center gap-1.5">
<kbd class="kbd"></kbd>
<kbd class="kbd"></kbd>
<span>Navigate</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="kbd">PgUp</kbd>
<kbd class="kbd">PgDn</kbd>
<span>Jump</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="kbd">Home</kbd>
<kbd class="kbd">End</kbd>
<span>First/Last</span>
</div>
<div class="flex items-center gap-1.5">
<kbd class="kbd">Enter</kbd>
<span>Resume</span>
</div>
<KeyboardHint shortcuts={[newSessionShortcut()]} separator="" />
</div>
</div>
</div>
)
}
export default InstanceWelcomeView