整理: 删除冗余的 src/ 目录,统一入口为 tui-plugins/,更新 README 和 package.json
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
node_modules/
|
||||
*.js
|
||||
src/
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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 };
|
||||
Reference in New Issue
Block a user