Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203import { createContext, useContext, useEffect, useState, useSyncExternalStore, type OctaneNode,} from "octane";import type { ConversationSummary } from "../../shared/conversations";import { createOwnerClient, OwnerSessionError } from "./owner-client";
type Connection = ReturnType<typeof createOwnerClient>;type View = { status: "connecting" | "connected" | "reconnecting" | "offline" | "unauthorized"; conversations: ConversationSummary[]; loading: boolean; creating: boolean; error: string;};const initial: View = { status: "connecting", conversations: [], loading: true, creating: false, error: "",};
/** Only navigation metadata lives here. Think owns conversations and running turns. */class ShellSession { private view = initial; private listeners = new Set<() => void>(); private connection: Connection | null = null; private active = false; private revision = 0; private retryTimer: ReturnType<typeof setTimeout> | undefined; getSnapshot = () => this.view; getServerSnapshot = () => initial; subscribe = (listener: () => void) => { this.listeners.add(listener); return () => { this.listeners.delete(listener); }; }; private publish(patch: Partial<View>) { this.view = { ...this.view, ...patch }; this.listeners.forEach((listener) => listener()); } start = () => { this.active = true; this.reconnect(); window.addEventListener("online", this.reconnect); window.addEventListener("offline", this.offline); window.addEventListener("focus", this.refresh); return () => { this.active = false; this.revision++; clearTimeout(this.retryTimer); this.connection?.close(); this.connection = null; window.removeEventListener("online", this.reconnect); window.removeEventListener("offline", this.offline); window.removeEventListener("focus", this.refresh); }; }; private offline = () => { this.revision++; clearTimeout(this.retryTimer); this.connection?.close(); this.connection = null; this.publish({ status: "offline", loading: false, creating: false }); }; reconnect = () => { if (!this.active) return; if (!navigator.onLine) { this.offline(); return; } clearTimeout(this.retryTimer); this.connection?.close(); const revision = ++this.revision; this.publish({ status: this.view.status === "connecting" ? "connecting" : "reconnecting", loading: true, creating: false, error: "", }); const connection = createOwnerClient(() => { if (!this.active || this.connection !== connection) return; this.revision++; connection.close(); this.connection = null; this.publish({ status: "reconnecting", loading: false, creating: false }); clearTimeout(this.retryTimer); this.retryTimer = setTimeout(this.reconnect, 3000); }); this.connection = connection; void connection.ready .then(async (client) => { client.addEventListener("message", (event) => { if (!this.active || this.connection !== connection) return; try { if (JSON.parse(String(event.data)).type === "conversations-changed") this.refresh(); } catch { // Other native protocol frames do not affect navigation metadata. } }); const conversations = await client.call<ConversationSummary[]>("listConversations"); if (!this.active || revision !== this.revision) return; this.publish({ status: "connected", conversations, loading: false }); }) .catch((error: unknown) => { if (!this.active || revision !== this.revision) return; connection.close(); this.connection = null; const unauthorized = error instanceof OwnerSessionError; this.publish({ status: unauthorized ? "unauthorized" : "reconnecting", loading: false, ...(unauthorized ? { conversations: [] } : {}), error: unauthorized ? error.message : "Could not reach Flarebot. Retrying…", }); if (!unauthorized) this.retryTimer = setTimeout(this.reconnect, 3000); }); }; refresh = () => { const connection = this.connection; if (!connection || this.view.status !== "connected") return; const revision = ++this.revision; void connection.ready .then((client) => client.call<ConversationSummary[]>("listConversations")) .then((conversations) => { if ( this.active && this.connection === connection && revision === this.revision ) this.publish({ conversations, error: "" }); }) .catch(() => { if ( this.active && this.connection === connection && revision === this.revision ) this.publish({ error: "Could not refresh conversations. Try again.", }); }); }; createConversation = async () => { const connection = this.connection; if (!connection || this.view.status !== "connected" || this.view.creating) return; this.publish({ creating: true, error: "" }); try { const client = await connection.ready; const conversation = await client.call<ConversationSummary>("createConversation"); if (!this.active || this.connection !== connection) return; this.revision++; this.publish({ conversations: [ conversation, ...this.view.conversations.filter( (item) => item.id !== conversation.id, ), ], }); return conversation; } catch { if (this.active && this.connection === connection) this.publish({ error: "Could not create a conversation. Your current conversation is still here. Refresh conversations before trying again.", }); } finally { if (this.active && this.connection === connection) this.publish({ creating: false }); } };}const Context = createContext<ShellSession | null>(null);export function ShellSessionProvider({ children }: { children: OctaneNode }) { const [session] = useState(() => new ShellSession()); useEffect(() => session.start(), [session]); return <Context.Provider value={session}>{children}</Context.Provider>;}export function useShellSession() { const session = useContext(Context); if (!session) throw new Error("Shell session provider is missing"); const view = useSyncExternalStore( session.subscribe, session.getSnapshot, session.getServerSnapshot, ); return { ...view, session };}