import { For, createSignal, createMemo, onMount, onCleanup } from "solid-js"; import { useKeyboard } from "@opentui/solid"; import type { KeyEvent } from "@opentui/core"; import type { Command } from "./registry"; import { spawnEditor } from "../util/editor.js"; import { RoomMode } from "../../../src/core/types/enums.js"; import type { RoomSummary } from "../../../src/core/types.js"; import { useKeyboardNav } from "../context/keyboard.js"; import { buildRoomYAMLTemplate, parseRoomYAML } from "../util/room-parser.js"; function modeBadge(mode: RoomMode): string { switch (mode) { case RoomMode.ChooseYourPath: return "[CYP]"; case RoomMode.FreeForAll: return "[FFA]"; case RoomMode.MessagesAgainstPersona: return "[MAP]"; default: return "[???]"; } } interface RoomListOverlayProps { rooms: RoomSummary[]; activeRoomId: string | null; onSelect: (roomId: string) => void; onDismiss: () => void; } function RoomListOverlay(props: RoomListOverlayProps) { const { setOverlayActive } = useKeyboardNav(); const [selectedIndex, setSelectedIndex] = createSignal(0); const [filterText, setFilterText] = createSignal(""); const [filterMode, setFilterMode] = createSignal(false); onMount(() => setOverlayActive(true)); onCleanup(() => setOverlayActive(false)); const filteredRooms = createMemo(() => { const filter = filterText().toLowerCase(); if (!filter) return props.rooms; return props.rooms.filter((r) => r.display_name.toLowerCase().includes(filter) ); }); createMemo(() => { const list = filteredRooms(); if (selectedIndex() >= list.length) { setSelectedIndex(Math.max(0, list.length - 1)); } }); useKeyboard((event: KeyEvent) => { const key = event.name; const listLength = filteredRooms().length; if (filterMode()) { if (key === "escape") { event.preventDefault(); setFilterText(""); setFilterMode(false); return; } if (key === "backspace") { event.preventDefault(); setFilterText((prev) => prev.slice(0, -1)); return; } if (key === "return") { event.preventDefault(); if (listLength > 0) { const selected = filteredRooms()[selectedIndex()]; props.onSelect(selected.id); } return; } if (key.length === 1 && !event.ctrl && !event.meta) { event.preventDefault(); setFilterText((prev) => prev + key); return; } } else { if (key === "j" || key === "down") { event.preventDefault(); setSelectedIndex((prev) => Math.min(prev + 1, listLength - 1)); return; } if (key === "k" || key === "up") { event.preventDefault(); setSelectedIndex((prev) => Math.max(prev - 1, 0)); return; } if (key === "return") { event.preventDefault(); if (listLength > 0) { const selected = filteredRooms()[selectedIndex()]; props.onSelect(selected.id); } return; } if (key === "escape") { event.preventDefault(); props.onDismiss(); return; } if (key === "/") { event.preventDefault(); setFilterMode(true); return; } } }); return ( Select Room {(room, index) => { const isActive = () => props.activeRoomId === room.id; const isSelected = () => selectedIndex() === index(); const label = () => { const prefix = isActive() ? "> " : " "; const badge = modeBadge(room.mode); const unread = room.unread_count > 0 ? ` (${room.unread_count})` : ""; return `${prefix}${badge} ${room.display_name}${unread}`; }; return ( {label()} ); }} {filterMode() ? ( Filter: {filterText()}| ) : ( j/k: navigate | Enter: select | Esc: cancel | /: filter )} ); } export const roomCommand: Command = { name: "room", aliases: ["r"], description: "List rooms, switch to a room, or create a new one", usage: "/room | /room | /room new", async execute(args, ctx) { const unarchived = ctx.ei.rooms().filter(r => !r.is_archived); if (args.length === 0) { if (unarchived.length === 0) { ctx.showNotification("No rooms. Use /room new to create one.", "info"); return; } ctx.showOverlay((hideOverlay, _hideForEditor) => ( { const room = unarchived.find(r => r.id === roomId); ctx.ei.selectRoom(roomId); hideOverlay(); ctx.showNotification(`Switched to ${room?.display_name ?? roomId}`, "info"); }} onDismiss={hideOverlay} /> ), ctx.renderer); return; } if (args[0].toLowerCase() === "new") { const personas = ctx.ei.personas(); const rawName = args.slice(1).join(" ").replace(/^["']|["']$/g, ""); const result = await spawnEditor({ initialContent: buildRoomYAMLTemplate(personas, rawName), filename: "new-room.yaml", renderer: ctx.renderer, }); if (result.aborted) { ctx.showNotification("Room creation cancelled", "info"); return; } if (!result.success || result.content === null) { ctx.showNotification("No changes — room not created", "info"); return; } try { const input = parseRoomYAML(result.content, personas); await ctx.ei.createRoom(input); ctx.showNotification(`Room "${input.display_name}" created`, "info"); } catch (err) { const msg = err instanceof Error ? err.message : String(err); ctx.showNotification(`Failed to create room: ${msg}`, "error"); } return; } const name = args.join(" "); const roomId = ctx.ei.resolveRoomName(name); if (roomId) { const room = unarchived.find(r => r.id === roomId); ctx.ei.selectRoom(roomId); ctx.showNotification(`Switched to ${room?.display_name ?? name}`, "info"); } else { ctx.showNotification(`No room named "${name}". Use /room new to create one.`, "warn"); } }, };