diff --git a/package.json b/package.json index f9f757e..7d9b016 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,12 @@ { "name": "oc-session-all", - "version": "2.0.0", - "description": "OpenCode TUI plugin: browse and switch to any session across all projects", - "main": "tui-plugins/oc-session-all.ts", + "version": "2.1.0", + "description": "OpenCode v2 TUI plugin: browse and switch to any session across all projects", + "main": "tui.js", "type": "module", "exports": { - ".": "./tui-plugins/oc-session-all.ts" + ".": "./tui.js", + "./tui": "./tui.js" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" @@ -20,13 +21,6 @@ "author": "", "license": "MIT", "engines": { - "opencode": ">=1.18.0" - }, - "dependencies": { - "@opencode-ai/plugin": "latest" - }, - "devDependencies": { - "@opentui/solid": "latest", - "typescript": "^5.0.0" + "opencode": ">=2.0.0" } -} \ No newline at end of file +} diff --git a/tui-plugins/oc-session-all.js b/tui-plugins/oc-session-all.js index 560c173..9b9c34e 100644 --- a/tui-plugins/oc-session-all.js +++ b/tui-plugins/oc-session-all.js @@ -1,15 +1,15 @@ /** - * OpenCode TUI Plugin: Show All Sessions + * OpenCode v2 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. */ +import { Plugin } from "@opencode/plugin/tui" + // ── Helpers ──────────────────────────────────────────────────────────── function formatDateTime(ms) { @@ -42,145 +42,140 @@ function projectDisplayName(session, projectMap) { // ── Plugin ───────────────────────────────────────────────────────────── -const tui = async (api) => { - // ─── MCP Error Toast Suppression ───────────────────────────────────── - api.event.on("tui.toast.show", (event) => { - 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")) - ) { - props.variant = "info"; - props.message = "MCP server offline (auto-reconnect when available)"; - props.duration = 2000; - } - }); +export default Plugin.define({ + id: "oc-session-all", + setup(context) { + console.log("[oc-session-all] v2 Plugin loaded!") - // ─── Session Picker Dialog ──────────────────────────────────────────── - const openPicker = async () => { - if (api.mode.current() !== "base") return; + // ─── MCP Error Toast Suppression ───────────────────────────────────── + context.data.on("tui.toast.show", (event) => { + 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")) + ) { + props.variant = "info"; + props.message = "MCP server offline (auto-reconnect when available)"; + props.duration = 2000; + } + }); - let sessions = []; - let projects = []; + // ─── Register Command ───────────────────────────────────────────────── + context.keymap.layer({ + commands: [ + { + 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(); + }, + }, + ], + bindings: [ + { + key: "ctrl+o", + desc: "All sessions picker", + preventDefault: true, + cmd: () => void openPicker(), + }, + ], + }); - try { - const [sessionResult, projectResult] = await Promise.all([ - api.client.experimental.session.list({ - limit: 1000, - roots: true, - directory: "", + // ─── Session Picker Dialog ──────────────────────────────────────────── + async function openPicker() { + let sessions = []; + let projects = []; + + try { + const [sessionResult, projectResult] = await Promise.all([ + context.client.experimental.session.list({ + limit: 1000, + roots: true, + directory: "", + }), + context.client.project.list({}), + ]); + + sessions = sessionResult?.data ?? sessionResult ?? []; + projects = projectResult?.data ?? projectResult ?? []; + } catch (err) { + context.ui.toast.show({ + message: "Failed to load sessions", + variant: "error", + }); + return; + } + + if (!Array.isArray(sessions) || !Array.isArray(projects)) return; + + const projectMap = new Map(projects.map((p) => [p.id, p])); + + const visible = sessions.filter( + (s) => !s.time?.archived && !s.parentID, + ); + + if (visible.length === 0) { + context.ui.toast.show({ + message: "No sessions found", + variant: "warning", + }); + return; + } + + 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); + } + + for (const list of grouped.values()) { + list.sort( + (a, b) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0), + ); + } + + const orderedProjects = [...grouped.keys()].sort((a, b) => + a.localeCompare(b), + ); + + const options = orderedProjects.flatMap((project) => + grouped.get(project).map((session) => { + 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, + }; }), - 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; - - const projectMap = new Map(projects.map((p) => [p.id, p])); - - const visible = sessions.filter( - (s) => !s.time?.archived && !s.parentID, - ); - - if (visible.length === 0) { - api.ui.toast({ - message: "No sessions found", - variant: "warning", - }); - return; - } - - 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); - } - - for (const list of grouped.values()) { - list.sort( - (a, b) => (b.time?.updated ?? 0) - (a.time?.updated ?? 0), + context.ui.dialog.setSize("xlarge"); + context.ui.dialog.replace( + () => + context.ui.DialogSelect({ + title: "All Sessions", + placeholder: "Search sessions across all projects\u2026", + options, + onSelect: (option) => { + context.ui.dialog.clear(); + context.route.navigate("session", { sessionID: option.value }); + }, + }), + () => {}, ); } - - const orderedProjects = [...grouped.keys()].sort((a, b) => - a.localeCompare(b), - ); - - const options = orderedProjects.flatMap((project) => - grouped.get(project).map((session) => { - 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, - }; - }), - ); - - 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) => { - api.ui.dialog.clear(); - api.route.navigate("session", { sessionID: option.value }); - }, - }), - () => {}, - ); - }; - - // ─── Register 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 ─────────────────────────────────────────────────────── - api.keymap.registerLayer({ - bindings: [ - { - key: "ctrl+o", - desc: "All sessions picker", - preventDefault: true, - cmd: () => void openPicker(), - }, - ], - }); -}; - -// ── Export ────────────────────────────────────────────────────────────── - -export default { id: "oc-session-all", tui }; + }, +})