整理: 删除冗余的 src/ 目录,统一入口为 tui-plugins/,更新 README 和 package.json

This commit is contained in:
ypshidifu
2026-09-21 19:41:05 +08:00
parent 6c4d97362f
commit 258a40b0d0
5 changed files with 33 additions and 424 deletions
+1
View File
@@ -1,2 +1,3 @@
node_modules/
*.js
src/
+30 -78
View File
@@ -1,96 +1,48 @@
# oc-session-all
OpenCode 插件:在任意目录下显示所有会话记录,绕过目录过滤限制。
OpenCode TUI 插件:弹出全项目会话选择器,跨所有项目浏览和切换会话。
## 功能
- 拦截 `/session` 命令,显示所有项目的会话记录,而不仅仅是当前目录的
- 提供 `/sessions-all` 命令作为替代方案
- 按更新时间倒序显示会话列表
- `Ctrl+o` 或 `/sessions-all` 打开可搜索的会话选择面板
- 按项目分组、按更新时间倒序排列
- 过滤掉已归档和子代理会话
- 拦截 MCP 连接错误 toast,保持界面整洁
## 安装
### 方法 1:全局安装(推荐)
在 `~/.config/opencode/tui.json` 中添加:
1. 将 `src/index.ts` 复制到全局插件目录:
```bash
# Windows
mkdir -p %USERPROFILE%\.config\opencode\plugins
copy src\index.ts %USERPROFILE%\.config\opencode\plugins\oc-session-all.ts
# Linux/macOS
mkdir -p ~/.config/opencode/plugins
cp src/index.ts ~/.config/opencode/plugins/oc-session-all.ts
```
2. 重启 OpenCode
### 方法 2:项目级安装
1. 将 `src/index.ts` 复制到项目目录:
```bash
mkdir -p .opencode/plugins
cp src/index.ts .opencode/plugins/oc-session-all.ts
```
2. 重启 OpenCode
## 使用方法
### 方法 1:拦截 /session 命令
插件会自动拦截 `/session` 命令,显示所有会话记录。
### 方法 2:使用新命令
运行 `/sessions-all` 命令来显示所有会话记录。
## 输出示例
```
=== All Sessions (across all projects) ===
ID: ses_12af0b69cffeAMR3VcfAdp94Hi
Title: OpenCode WSL 迁移
Directory: C:/Users/ypshidifu
Created: 2026/9/20 10:00:00
Updated: 2026/9/20 15:30:00
---
ID: ses_12aeb915effez1zEjjIqf6yl1c
Title: Session 2
Directory: D:/桌面/源码/mc_plugn/windowmsgbot
Created: 2026/9/20 09:00:00
Updated: 2026/9/20 14:00:00
---
Total: 68 sessions
```json
{
"$schema": "https://opencode.ai/tui.json",
"plugin": [
"D:/桌面/源码/oc_session/tui-plugins/oc-session-all.ts"
]
}
```
## 工作原理
重启 OpenCode 即可生效。
插件通过两种方式获取所有会话:
## 快捷键
1. **OpenCode Client API**:尝试使用 OpenCode 的客户端 API 获取会话列表
2. **数据库直接查询**:如果 API 不可用,直接查询 SQLite 数据库
| 快捷键 | 命令 | 说明 |
|--------|------|------|
| `Ctrl+o` | — | 打开会话选择器 |
| `/sessions-all` | `resume-all`, `continue-all` | 斜杠命令 |
## 配置
## 项目结构
插件无需额外配置,安装后即可使用。
## 故障排除
如果插件不工作:
1. 确保插件文件已正确放置在插件目录
2. 重启 OpenCode
3. 检查控制台日志是否有错误信息
4. 确保 SQLite3 命令行工具可用(用于数据库查询回退方案)
## 依赖
- OpenCode 1.18.11 或更新版本
- SQLite3 命令行工具(可选,用于数据库查询回退方案)
```
oc_session/
├── tui-plugins/
│ └── oc-session-all.ts ← 插件源码(OC 加载入口)
├── package.json
├── node_modules/
├── .gitignore
└── README.md
```
## 许可证
MIT
MIT
+2 -4
View File
@@ -2,14 +2,12 @@
"name": "oc-session-all",
"version": "2.0.0",
"description": "OpenCode TUI plugin: browse and switch to any session across all projects",
"main": "src/index.ts",
"main": "tui-plugins/oc-session-all.ts",
"type": "module",
"exports": {
".": "./src/index.ts",
"./tui": "./src/index.ts"
".": "./tui-plugins/oc-session-all.ts"
},
"scripts": {
"build": "tsc",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
-134
View File
@@ -1,134 +0,0 @@
/**
* OpenCode Plugin: Show All Sessions (JavaScript version)
*
* This plugin provides /sessions-all command to show all sessions
* regardless of the current working directory.
*/
const { exec } = require("child_process");
const { promisify } = require("util");
const { join } = require("path");
const { existsSync } = require("fs");
const execAsync = promisify(exec);
/**
* Get the path to the opencode database
*/
function getDbPath() {
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
// Windows and Linux/macOS path (opencode uses ~/.local/share/opencode/opencode.db on all platforms)
const dbPath = join(homeDir, ".local", "share", "opencode", "opencode.db");
if (existsSync(dbPath)) return dbPath;
// Fallback to Windows AppData (older versions)
const winPath = join(homeDir, "AppData", "Roaming", "opencode", "opencode.db");
if (existsSync(winPath)) return winPath;
// Final fallback
return dbPath;
}
/**
* Query all sessions from the database
*/
async function getAllSessionsFromDb() {
try {
const dbPath = getDbPath();
const query = `SELECT id, project_id, directory, title, time_created, time_updated, cost FROM session_v2 ORDER BY time_updated DESC;`;
const { stdout } = await execAsync(`sqlite3 "${dbPath}" "${query}"`);
if (!stdout.trim()) {
return [];
}
const sessions = [];
const lines = stdout.trim().split("\n");
for (const line of lines) {
const parts = line.split("|");
if (parts.length >= 7) {
sessions.push({
id: parts[0],
project_id: parts[1],
directory: parts[2],
title: parts[3] || null,
time_created: parseInt(parts[4]) || 0,
time_updated: parseInt(parts[5]) || 0,
cost: parseFloat(parts[6]) || 0,
});
}
}
return sessions;
} catch (error) {
console.error("[oc-session-all] Failed to query database:", error);
return [];
}
}
/**
* Format a timestamp to local string
*/
function formatTime(timestamp) {
if (!timestamp) return "Unknown";
return new Date(timestamp * 1000).toLocaleString("zh-CN");
}
/**
* Format cost to display string
*/
function formatCost(cost) {
if (!cost) return "Free";
return `$${cost.toFixed(4)}`;
}
/**
* OpenCode Plugin
*/
const OcSessionAll = async ({ project, client, $, directory, worktree }) => {
console.log("[oc-session-all] Plugin loaded");
return {
tool: {
"sessions-all": {
description: "显示所有项目的会话记录,绕过目录过滤限制。Show all sessions from all projects.",
args: {},
async execute(args, context) {
const sessions = await getAllSessionsFromDb();
if (!sessions || sessions.length === 0) {
return "没有找到任何会话记录。No sessions found.";
}
let output = "=== 所有会话记录 (All Sessions) ===\n\n";
// Display sessions
let index = 1;
for (const session of sessions) {
const created = formatTime(session.time_created);
const updated = formatTime(session.time_updated);
const cost = formatCost(session.cost);
output += `${index}. ${session.title || "Untitled"}\n`;
output += ` ID: ${session.id}\n`;
output += ` 目录: ${session.directory}\n`;
output += ` 创建: ${created} | 更新: ${updated}\n`;
output += ` 费用: ${cost}\n\n`;
index++;
}
output += `共 ${sessions.length} 个会话`;
return output;
}
}
}
};
};
module.exports = OcSessionAll;
-208
View File
@@ -1,208 +0,0 @@
/**
* 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, any>,
): 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<string, any[]>();
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 };