/** * OpenCode TUI Plugin: Show All Sessions * * Registers /sessions-all command to show all sessions * across all projects via a searchable picker dialog. * Selecting a session navigates directly to it. * * TUI format: export default { id, tui } * * Also intercepts MCP connection error toasts to keep them quiet. */ // ── Helpers ──────────────────────────────────────────────────────────── function formatDateTime(ms: number): string { const d = new Date(ms); const pad = (v: number) => String(v).padStart(2, "0"); const month = pad(d.getMonth() + 1); const day = pad(d.getDate()); const hour = pad(d.getHours()); const min = pad(d.getMinutes()); return `${d.getFullYear()}/${month}/${day} ${hour}:${min}`; } function projectDisplayName( session: any, projectMap: Map, ): string { const project = session.projectID ? projectMap.get(session.projectID) : undefined; // Try project name first if (project?.name?.trim()) return project.name.trim(); // Try worktree basename const worktree = project?.worktree ?.replace(/\/+$/, "") .split(/[\\/]/) .pop(); if (worktree) return worktree; // Try directory basename const dir = session.directory ?.replace(/\/+$/, "") .split(/[\\/]/) .pop(); if (dir) return dir; // Fallback to truncated project ID return session.projectID ? session.projectID.slice(0, 8) : "unknown"; } // ── Plugin ───────────────────────────────────────────────────────────── const tui = async (api: any) => { // ─── MCP Error Toast Suppression ───────────────────────────────────── // Listen for toast events and suppress MCP-related error toasts. // This keeps the UI clean when baota-mcp server is offline. // When the server comes back online, tools auto-reconnect. api.event.on("tui.toast.show", (event: any) => { const props = event.properties; if ( props.variant === "error" && typeof props.message === "string" && (props.message.toLowerCase().includes("mcp") || props.message.toLowerCase().includes("mcp server") || props.message.toLowerCase().includes("mcp error") || props.message.toLowerCase().includes("failed to connect") || props.message.toLowerCase().includes("connection refused")) ) { // Replace the error message with a silent info toast props.variant = "info"; props.message = "MCP server offline (auto-reconnect when available)"; props.duration = 2000; } }); // ─── Session Picker Dialog ──────────────────────────────────────────── const openPicker = async () => { // Only work in base mode (not in a permission dialog etc.) if (api.mode.current() !== "base") return; let sessions: any[] = []; let projects: any[] = []; try { // Use experimental.session.list to get all sessions across projects const [sessionResult, projectResult] = await Promise.all([ api.client.experimental.session.list({ limit: 1000, roots: true, directory: "", }), api.client.project.list({}), ]); sessions = sessionResult?.data ?? sessionResult ?? []; projects = projectResult?.data ?? projectResult ?? []; } catch (err) { api.ui.toast({ message: "Failed to load sessions", variant: "error", }); return; } if (!Array.isArray(sessions) || !Array.isArray(projects)) return; // Build project lookup map const projectMap = new Map(projects.map((p: any) => [p.id, p])); // Filter: hide archived and subagent sessions const visible = sessions.filter( (s: any) => !s.time?.archived && !s.parentID, ); if (visible.length === 0) { api.ui.toast({ message: "No sessions found", variant: "warning", }); return; } // Group by project const grouped = new Map(); for (const session of visible) { const name = projectDisplayName(session, projectMap); const list = grouped.get(name) ?? []; list.push(session); grouped.set(name, list); } // Sort sessions within each project by update time (newest first) for (const list of grouped.values()) { list.sort( (a: any, b: any) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0), ); } // Sort projects alphabetically const orderedProjects = [...grouped.keys()].sort((a, b) => a.localeCompare(b), ); // Build dialog options const options = orderedProjects.flatMap((project) => grouped.get(project)!.map((session: any) => { const updated = session.time?.updated ? formatDateTime(session.time.updated) : "unknown"; const title = session.title || "Untitled"; return { title: `${project} ${updated} \u2502 ${title}`, value: session.id, category: project, }; }), ); // Show the dialog api.ui.dialog.setSize("xlarge"); api.ui.dialog.replace( () => api.ui.DialogSelect({ title: "All Sessions", placeholder: "Search sessions across all projects\u2026", options, onSelect: (option: any) => { api.ui.dialog.clear(); api.route.navigate("session", { sessionID: option.value }); }, }), () => {}, ); }; // ─── Register Command ───────────────────────────────────────────────── // Register /sessions-all as a slash command if (api.command) { api.command.register(() => [ { title: "All Sessions", value: "sessions.all", description: "Browse and switch to any session across all projects", category: "Session Picker", slash: { name: "sessions-all", aliases: ["resume-all", "continue-all"] }, onSelect: () => { void openPicker(); }, }, ]); } // ─── Keybinding ─────────────────────────────────────────────────────── // Ctrl+o: dedicated fallback keybinding to open the picker api.keymap.registerLayer({ bindings: [ { key: "ctrl+o", desc: "All sessions picker", preventDefault: true, cmd: () => void openPicker(), }, ], }); }; // ── Export ────────────────────────────────────────────────────────────── export default { id: "oc-session-all", tui };