import { Show, createMemo, createEffect, createSignal, type Component } from "solid-js" import type { Session } from "../../types/session" import type { Attachment } from "../../types/attachment" import type { ClientPart } from "../../types/message" import MessageSection from "../message-section" import { messageStoreBus } from "../../stores/message-v2/bus" import PromptInput from "../prompt-input" import { instances } from "../../stores/instances" import { loadMessages, sendMessage, forkSession, isSessionMessagesLoading, setActiveParentSession, setActiveSession, runShellCommand, abortSession } from "../../stores/sessions" import { isSessionBusy as getSessionBusyStatus } from "../../stores/session-status" import { showAlertDialog } from "../../stores/alerts" import { getLogger } from "../../lib/logger" const log = getLogger("session") function isTextPart(part: ClientPart): part is ClientPart & { type: "text"; text: string } { return part?.type === "text" && typeof (part as any).text === "string" } interface SessionViewProps { sessionId: string activeSessions: Map instanceId: string instanceFolder: string escapeInDebounce: boolean showSidebarToggle?: boolean onSidebarToggle?: () => void forceCompactStatusLayout?: boolean isActive: boolean } export const SessionView: Component = (props) => { const session = () => props.activeSessions.get(props.sessionId) const messagesLoading = createMemo(() => isSessionMessagesLoading(props.instanceId, props.sessionId)) const messageStore = createMemo(() => messageStoreBus.getOrCreate(props.instanceId)) const [scrollToBottomHandle, setScrollToBottomHandle] = createSignal<(() => void) | null>(null) createEffect(() => { if (!props.isActive) return const handler = scrollToBottomHandle() if (!handler) return requestAnimationFrame(() => requestAnimationFrame(() => handler())) }) const sessionBusy = createMemo(() => { const currentSession = session() if (!currentSession) return false return getSessionBusyStatus(props.instanceId, currentSession.id) }) let quoteHandler: ((text: string, mode: "quote" | "code") => void) | null = null createEffect(() => { const currentSession = session() if (currentSession) { loadMessages(props.instanceId, currentSession.id).catch((error) => log.error("Failed to load messages", error)) } }) function registerQuoteHandler(handler: (text: string, mode: "quote" | "code") => void) { quoteHandler = handler return () => { if (quoteHandler === handler) { quoteHandler = null } } } function handleQuoteSelection(text: string, mode: "quote" | "code") { if (quoteHandler) { quoteHandler(text, mode) } } async function handleSendMessage(prompt: string, attachments: Attachment[]) { const handler = scrollToBottomHandle() if (handler) { handler() } await sendMessage(props.instanceId, props.sessionId, prompt, attachments) } async function handleRunShell(command: string) { await runShellCommand(props.instanceId, props.sessionId, command) } async function handleAbortSession() { const currentSession = session() if (!currentSession) return try { await abortSession(props.instanceId, currentSession.id) log.info("Abort requested", { instanceId: props.instanceId, sessionId: currentSession.id }) } catch (error) { log.error("Failed to abort session", error) showAlertDialog("Failed to stop session", { title: "Stop failed", detail: error instanceof Error ? error.message : String(error), variant: "error", }) } } function getUserMessageText(messageId: string): string | null { const normalizedMessage = messageStore().getMessage(messageId) if (normalizedMessage && normalizedMessage.role === "user") { const parts = normalizedMessage.partIds .map((partId) => normalizedMessage.parts[partId]?.data) .filter((part): part is ClientPart => Boolean(part)) const textParts = parts.filter(isTextPart) if (textParts.length > 0) { return textParts.map((part) => part.text).join("\n") } } return null } async function handleRevert(messageId: string) { const instance = instances().get(props.instanceId) if (!instance || !instance.client) return try { await instance.client.session.revert({ path: { id: props.sessionId }, body: { messageID: messageId }, }) const restoredText = getUserMessageText(messageId) if (restoredText) { const textarea = document.querySelector(".prompt-input") as HTMLTextAreaElement if (textarea) { textarea.value = restoredText textarea.dispatchEvent(new Event("input", { bubbles: true })) textarea.focus() } } } catch (error) { log.error("Failed to revert message", error) showAlertDialog("Failed to revert to message", { title: "Revert failed", variant: "error", }) } } async function handleFork(messageId?: string) { if (!messageId) { log.warn("Fork requires a user message id") return } const restoredText = getUserMessageText(messageId) try { const forkedSession = await forkSession(props.instanceId, props.sessionId, { messageId }) const parentToActivate = forkedSession.parentId ?? forkedSession.id setActiveParentSession(props.instanceId, parentToActivate) if (forkedSession.parentId) { setActiveSession(props.instanceId, forkedSession.id) } await loadMessages(props.instanceId, forkedSession.id).catch((error) => log.error("Failed to load forked session messages", error)) if (restoredText) { const textarea = document.querySelector(".prompt-input") as HTMLTextAreaElement if (textarea) { textarea.value = restoredText textarea.dispatchEvent(new Event("input", { bubbles: true })) textarea.focus() } } } catch (error) { log.error("Failed to fork session", error) showAlertDialog("Failed to fork session", { title: "Fork failed", variant: "error", }) } } return (
Session not found
} > {(sessionAccessor) => { const activeSession = sessionAccessor() if (!activeSession) return null return (
{ setScrollToBottomHandle(() => fn) if (props.isActive) { requestAnimationFrame(() => requestAnimationFrame(() => fn())) } }} showSidebarToggle={props.showSidebarToggle} onSidebarToggle={props.onSidebarToggle} forceCompactStatusLayout={props.forceCompactStatusLayout} onQuoteSelection={handleQuoteSelection} />
) }}
) } export default SessionView