From d6048da95fb8732738fe99ef520f35048172df3b Mon Sep 17 00:00:00 2001 From: ypshidifu Date: Tue, 22 Sep 2026 22:21:56 +0800 Subject: [PATCH] =?UTF-8?q?v5:=20=E6=8C=89=E5=AE=98=E6=96=B9=20opencode.bt?= =?UTF-8?q?w=20=E6=A8=A1=E6=9D=BF=E6=B3=A8=E5=86=8C=E5=91=BD=E4=BB=A4=20?= =?UTF-8?q?=E2=80=94=20ui.slot({append:app,render})=20+=20keymap.layer(()?= =?UTF-8?q?=3D>({mode,commands}))=EF=BC=8C=E6=81=A2=E5=A4=8D=E5=85=A8?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=88sessions-all=20slash=E5=91=BD?= =?UTF-8?q?=E4=BB=A4+ctrl+o+=E9=80=89=E6=8B=A9=E5=99=A8+MCP=20toast?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tui-plugins/oc-session-all.js | 276 +++++++++++++++++++++++----------- 1 file changed, 186 insertions(+), 90 deletions(-) diff --git a/tui-plugins/oc-session-all.js b/tui-plugins/oc-session-all.js index a020949..87c1a9e 100644 --- a/tui-plugins/oc-session-all.js +++ b/tui-plugins/oc-session-all.js @@ -2,132 +2,228 @@ import { Plugin } from "@opencode/plugin/tui" import { appendFileSync, mkdirSync, existsSync } from "fs" import { join } from "path" -// 日志文件路径 +// ── 日志 ────────────────────────────────────────────────────────────── const LOG_DIR = "C:\\Users\\ypshidifu\\.config\\opencode" const LOG_FILE = join(LOG_DIR, "oc-session-all.log") - -// 确保日志目录存在 if (!existsSync(LOG_DIR)) { mkdirSync(LOG_DIR, { recursive: true }) } - function log(msg) { - const timestamp = new Date().toISOString() - const line = `[${timestamp}] ${msg}\n` try { - appendFileSync(LOG_FILE, line) - } catch (e) { - // 忽略写入错误 - } + appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`) + } catch (e) {} console.log(msg) } - -// 延时函数 function delay(ms) { - return new Promise(resolve => setTimeout(resolve, ms)) + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +// ── Helpers(移植自 v1)──────────────────────────────────────────────── +function formatDateTime(ms) { + const d = new Date(ms) + const pad = (v) => String(v).padStart(2, "0") + return `${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}` +} + +function projectDisplayName(session, projectMap) { + const project = session.projectID ? projectMap.get(session.projectID) : undefined + if (project?.name?.trim()) return project.name.trim() + const worktree = project?.worktree?.replace(/\/+$/, "").split(/[\\/]/).pop() + if (worktree) return worktree + const dir = session.directory?.replace(/\/+$/, "").split(/[\\/]/).pop() + if (dir) return dir + const locDir = session.location?.directory?.replace(/\/+$/, "").split(/[\\/]/).pop() + if (locDir) return locDir + return session.projectID ? session.projectID.slice(0, 8) : "unknown" } export default Plugin.define({ id: "oc-session-all", async setup(context) { - log("[oc-session-all] v4 Plugin loaded! Calling ui.slot(component) directly...") + log("[oc-session-all] v5 loaded — official slot({append}) + layer(()=>({})) pattern") - // 延时 5 秒,避免 toast 被覆盖(别人插件也会弹) - await delay(5000) - - const report = [] - - // ── 探测 ui.slot 函数本身的元信息 ────────────────────────────────────── - if (context.ui && typeof context.ui.slot === "function") { - report.push("ui.slot fn name=" + context.ui.slot.name + " arity=" + context.ui.slot.length) - log("[oc-session-all] ui.slot is a function, name=" + context.ui.slot.name + ", arity=" + context.ui.slot.length) - } else { - report.push("ui.slot NOT a function") - log("[oc-session-all] ui.slot is NOT a function, type=" + typeof (context.ui && context.ui.slot)) + // ── MCP error toast 拦截(monkey-patch toast.show,无未验证 API)── + try { + const toast = context.ui.toast + const orig = toast.show.bind(toast) + toast.show = (opts) => { + try { + const msg = String(opts?.message ?? "").toLowerCase() + if ( + opts?.variant === "error" && + (msg.includes("mcp") || + msg.includes("failed to connect") || + msg.includes("connection refused")) + ) { + return orig({ + ...opts, + variant: "info", + message: "MCP server offline (auto-reconnect when available)", + duration: 2000, + }) + } + } catch (e) { + log("[oc-session-all] toast interceptor check ERROR: " + e.message) + } + return orig(opts) + } + log("[oc-session-all] MCP toast interceptor installed") + } catch (e) { + log("[oc-session-all] toast interceptor install ERROR: " + e.message) } - // ── 直接调用 ui.slot(component) 注册组件 ────────────────────────────────────── - let slotReturn = "not called" - try { - // 组件在 SolidJS 渲染上下文内执行,此时 Keymap.Provider 应该可用 - const ProbeComponent = () => { - log("[oc-session-all] ProbeComponent RENDERED inside slot!") - // 在组件渲染阶段尝试 keymap.layer(此时应在 Provider 内) + // ── Session 选择器(移植自 v1,改用 v2 dialog.select/router API)── + const openPicker = async () => { + log("[oc-session-all] openPicker called") + try { + let sessions = [] + let projects = [] + + // 数据源 1:client SDK try { - if (context.keymap && typeof context.keymap.layer === "function") { - const layerResult = context.keymap.layer({ + const r = await context.client.session.list({ limit: 1000, order: "desc" }) + sessions = (r && r.data) || r || [] + log("[oc-session-all] client.session.list -> " + (Array.isArray(sessions) ? sessions.length + " items" : "non-array")) + } catch (e) { + log("[oc-session-all] client.session.list ERROR: " + e.message) + } + + // 数据源 2:data store 回退(内置 /sessions 也用它) + if (!Array.isArray(sessions) || sessions.length === 0) { + try { + sessions = context.data.session.list() || [] + log("[oc-session-all] data.session.list fallback -> " + sessions.length + " items") + } catch (e) { + log("[oc-session-all] data.session.list ERROR: " + e.message) + } + } + + try { + const r = await context.client.project.list({}) + projects = (r && r.data) || r || [] + } catch (e) { + log("[oc-session-all] client.project.list ERROR: " + e.message) + } + + if (!Array.isArray(sessions) || sessions.length === 0) { + context.ui.toast.show({ message: "No sessions found", variant: "warning" }) + return + } + + const projectMap = new Map( + (Array.isArray(projects) ? 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) => ({ + title: `${project} ${ + session.time?.updated ? formatDateTime(session.time.updated) : "unknown" + } │ ${session.title || "Untitled"}`, + value: session.id, + category: project, + })), + ) + log("[oc-session-all] picker options built: " + options.length) + + // v2 dialog.select 返回 Promise + const selected = await context.ui.dialog.select({ + title: "All Sessions", + placeholder: "Search sessions across all projects…", + options, + }) + log("[oc-session-all] dialog.select resolved: " + String(selected)) + if (selected) { + context.ui.router.navigate({ type: "session", sessionID: selected }) + } + } catch (e) { + log("[oc-session-all] openPicker ERROR: " + (e.stack || e.message)) + try { + context.ui.toast.show({ + message: "Failed to load sessions: " + e.message, + variant: "error", + }) + } catch (e2) {} + } + } + + // ── 命令注册:官方 opencode.btw 插件同款写法 ────────────────────── + try { + context.ui.slot({ + append: "app", + render() { + log("[oc-session-all] slot render called — registering keymap layer") + try { + return context.keymap.layer(() => ({ + mode: "global", commands: [ { - id: "sessions-all", - title: "Show all sessions", + id: "sessions.all", + title: "All Sessions", + description: "Browse and switch to any session across all projects", group: "Session", - run: () => { - log("[oc-session-all] /sessions-all command RUN!") + palette: true, + bind: "ctrl+o", + slash: { + name: "sessions-all", + aliases: ["resume-all", "continue-all"], + }, + run() { + void openPicker() }, }, ], - bindings: [ - { - key: "ctrl+o", - command: "sessions-all", - }, - ], - }) - log("[oc-session-all] keymap.layer() inside component: SUCCESS, result type=" + typeof layerResult) - report.push("layer-in-component: SUCCESS") - } else { - log("[oc-session-all] keymap.layer not available inside component") - report.push("layer-in-component: keymap.layer missing") + })) + } catch (e) { + log("[oc-session-all] layer inside render ERROR: " + e.message) + return null } - } catch (e) { - log("[oc-session-all] keymap.layer() inside component ERROR: " + e.message) - report.push("layer-in-component ERROR: " + e.message) - } - // 组件不渲染任何可见内容 - return null - } - - // 直接调用 slot 函数注册组件 - const ret = context.ui.slot(ProbeComponent) - slotReturn = typeof ret - report.push("ui.slot(component) returned type=" + typeof ret) - log("[oc-session-all] ui.slot(component) called, returned type=" + typeof ret + ", value=" + String(ret)) + }, + }) + log("[oc-session-all] ui.slot({append:'app'}) registered OK") } catch (e) { - report.push("ui.slot(component) ERROR: " + e.message) - log("[oc-session-all] ui.slot(component) ERROR: " + e.message) + log("[oc-session-all] ui.slot ERROR: " + e.message) } - // 等待组件渲染 - await delay(3000) - - // ── 验证命令是否注册成功 ────────────────────────────────────── + // ── 验证命令注册结果(写日志,不依赖 toast)──────────────────────── + await delay(4000) try { - if (context.keymap && typeof context.keymap.commands === "function") { - const cmds = context.keymap.commands() - const cmdStr = JSON.stringify(cmds) - log("[oc-session-all] commands() after slot: " + cmdStr.substring(0, 800)) - if (cmdStr.includes("sessions-all")) { - report.push("sessions-all REGISTERED!") - log("[oc-session-all] === sessions-all command REGISTERED SUCCESSFULLY ===") - } else { - report.push("sessions-all NOT in commands()") - } - } + const cmds = context.keymap.commands() + const s = JSON.stringify(cmds) + const ok = s.includes("sessions.all") || s.includes("sessions-all") + log("[oc-session-all] VERIFY commands() contains sessions-all: " + ok) + if (!ok) log("[oc-session-all] VERIFY commands() dump: " + s.substring(0, 600)) } catch (e) { - log("[oc-session-all] commands() check ERROR: " + e.message) + log("[oc-session-all] VERIFY commands() ERROR: " + e.message) } - // 最终报告 - const finalReport = report.join(" | ") - log("[oc-session-all] SLOT PROBE: " + finalReport) - - // 延时后显示 toast - await delay(2000) + // toast 延时显示(避免被其他插件覆盖) + await delay(3000) try { context.ui.toast.show({ - message: "Slot probe: " + finalReport, + message: "oc-session-all v5 loaded — /sessions-all or Ctrl+O", variant: "info", - duration: 25000, + duration: 20000, }) } catch (e) { log("[oc-session-all] toast.show ERROR: " + e.message)