diff --git a/extension.js b/extension.js
index cd463a0..d88129b 100644
--- a/extension.js
+++ b/extension.js
@@ -6,9 +6,13 @@ const fsp = require("node:fs/promises");
const readline = require("node:readline");
const { pipeline } = require("node:stream/promises");
-const VIEW_TYPE = "codexSessionManager.panel";
-const SIDEBAR_CONTAINER_ID = "codexSessionManager";
-const SIDEBAR_VIEW_ID = "codexSessionManager.main";
+const VIEW_IDS = {
+ container: "codexSessionManager",
+ controls: "codexSessionManager.controls",
+ sessions: "codexSessionManager.sessions",
+ details: "codexSessionManager.details",
+ messages: "codexSessionManager.messages",
+};
let sqliteModCache;
let resumeTerminal = null;
@@ -26,18 +30,29 @@ function getSqliteModule() {
}
function activate(context) {
- let panelRef = null;
+ const store = new SessionManagerStore(context);
+ const sessionsProvider = new SessionsTreeDataProvider(store);
- const provider = {
- resolveWebviewView(webviewView) {
- setupWebview(webviewView.webview, context);
- },
- };
+ const controlsProvider = new ControlsWebviewProvider(context, store);
+ const detailsProvider = new DetailsWebviewProvider(context, store);
+ const messagesProvider = new MessagesWebviewProvider(context, store);
context.subscriptions.push(
- vscode.window.registerWebviewViewProvider(SIDEBAR_VIEW_ID, provider, {
+ sessionsProvider,
+ controlsProvider,
+ detailsProvider,
+ messagesProvider,
+ vscode.window.registerTreeDataProvider(VIEW_IDS.sessions, sessionsProvider),
+ vscode.window.registerWebviewViewProvider(VIEW_IDS.controls, controlsProvider, {
+ webviewOptions: { retainContextWhenHidden: true },
+ }),
+ vscode.window.registerWebviewViewProvider(VIEW_IDS.details, detailsProvider, {
+ webviewOptions: { retainContextWhenHidden: true },
+ }),
+ vscode.window.registerWebviewViewProvider(VIEW_IDS.messages, messagesProvider, {
webviewOptions: { retainContextWhenHidden: true },
}),
+ store,
);
context.subscriptions.push(
@@ -49,88 +64,793 @@ function activate(context) {
);
const openCmd = vscode.commands.registerCommand("codexSessionManager.open", async () => {
+ await vscode.commands.executeCommand(`workbench.view.extension.${VIEW_IDS.container}`);
+ await vscode.commands.executeCommand(`${VIEW_IDS.sessions}.focus`);
+ });
+
+ const refreshCmd = vscode.commands.registerCommand("codexSessionManager.refresh", async () => {
+ await store.refreshAll({ preserveSelection: true });
+ });
+
+ const selectSessionCmd = vscode.commands.registerCommand("codexSessionManager.selectSession", async (id) => {
+ await store.selectSession(String(id || ""));
+ });
+
+ context.subscriptions.push(openCmd, refreshCmd, selectSessionCmd);
+ void store.initialize();
+}
+
+function deactivate() {}
+
+function getConfig() {
+ const cfg = vscode.workspace.getConfiguration("codexSessionManager");
+ return {
+ codexHome: String(cfg.get("codexHome") || "").trim(),
+ };
+}
+
+function resolveCodexHome(configuredHome) {
+ if (configuredHome) {
+ if (configuredHome.startsWith("~")) {
+ return path.join(os.homedir(), configuredHome.slice(1));
+ }
+ return configuredHome;
+ }
+ return path.join(os.homedir(), ".codex");
+}
+
+function getEnvironment() {
+ const cfg = getConfig();
+ const codexHome = resolveCodexHome(cfg.codexHome);
+ return {
+ codexHome,
+ dbPath: path.join(codexHome, "state_5.sqlite"),
+ };
+}
+
+function formatDisplayTime(value) {
+ if (!value) {
+ return "-";
+ }
+ const date = new Date(value);
+ if (Number.isNaN(date.getTime())) {
+ return String(value);
+ }
+ return date.toLocaleString();
+}
+
+function escapeHtml(text) {
+ return String(text ?? "").replace(/[&<>"']/g, (ch) => {
+ const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
+ return map[ch] || ch;
+ });
+}
+
+function shortId(id) {
+ const raw = String(id || "");
+ return raw.length > 14 ? `${raw.slice(0, 8)}...${raw.slice(-4)}` : raw;
+}
+
+class SessionTreeItem extends vscode.TreeItem {
+ constructor(session, selectedId) {
+ super(session.title || session.firstUserMessage || session.id, vscode.TreeItemCollapsibleState.None);
+ this.id = session.id;
+ this.description = `${session.provider || "(empty)"} · ${formatDisplayTime(session.updatedAt)}`;
+ this.tooltip = `${session.id}\n${session.cwd || ""}`.trim();
+ this.command = {
+ command: "codexSessionManager.selectSession",
+ title: "Select Session",
+ arguments: [session.id],
+ };
+ this.contextValue = session.archived ? "archivedSession" : "activeSession";
+ this.iconPath = new vscode.ThemeIcon(
+ session.id === selectedId ? "circle-filled" : session.providerMismatch ? "warning" : "comment-discussion",
+ new vscode.ThemeColor(session.providerMismatch ? "problemsWarningIcon.foreground" : "list.activeSelectionIconForeground"),
+ );
+ }
+}
+
+class SessionsTreeDataProvider {
+ constructor(store) {
+ this.store = store;
+ this._onDidChangeTreeData = new vscode.EventEmitter();
+ this.onDidChangeTreeData = this._onDidChangeTreeData.event;
+ this.disposable = store.onDidChange(() => this._onDidChangeTreeData.fire());
+ }
+
+ dispose() {
+ this.disposable.dispose();
+ this._onDidChangeTreeData.dispose();
+ }
+
+ getTreeItem(item) {
+ return item;
+ }
+
+ getChildren() {
+ const state = this.store.getState();
+ return state.items.map((item) => new SessionTreeItem(item, state.selectedId));
+ }
+}
+
+class SessionManagerStore {
+ constructor(context) {
+ this.context = context;
+ this._onDidChange = new vscode.EventEmitter();
+ this.onDidChange = this._onDidChange.event;
+ this.state = {
+ codexHome: "",
+ dbPath: "",
+ dbExists: false,
+ mode: "active",
+ search: "",
+ mismatchOnly: false,
+ items: [],
+ listTotal: 0,
+ mismatchCount: 0,
+ selectedId: "",
+ detail: null,
+ sessionHealth: null,
+ configInfo: null,
+ statusText: "就绪",
+ statusType: "info",
+ };
+ }
+
+ dispose() {
+ this._onDidChange.dispose();
+ }
+
+ getState() {
+ return { ...this.state };
+ }
+
+ emitChange() {
+ this._onDidChange.fire(this.getState());
+ }
+
+ setStatus(text, type = "info") {
+ this.state.statusText = text;
+ this.state.statusType = type;
+ this.emitChange();
+ }
+
+ updateEnvironment() {
+ const env = getEnvironment();
+ this.state.codexHome = env.codexHome;
+ this.state.dbPath = env.dbPath;
+ this.state.dbExists = fs.existsSync(env.dbPath);
+ return env;
+ }
+
+ async initialize() {
try {
- await vscode.commands.executeCommand(`workbench.view.extension.${SIDEBAR_CONTAINER_ID}`);
- await vscode.commands.executeCommand(`${SIDEBAR_VIEW_ID}.focus`);
+ this.updateEnvironment();
+ await this.loadConfigProviders();
+ await this.loadList({ keepSelection: false });
+ if (!this.state.dbExists) {
+ this.setStatus(`未找到数据库: ${this.state.dbPath}`, "error");
+ } else {
+ this.setStatus(`就绪 · ${this.state.codexHome}`, "success");
+ }
+ } catch (error) {
+ this.setStatus(`初始化失败: ${error.message}`, "error");
+ }
+ }
+
+ async refreshAll(options = {}) {
+ this.updateEnvironment();
+ await this.loadConfigProviders();
+ await this.loadList({ keepSelection: options.preserveSelection !== false });
+ if (this.state.selectedId) {
+ await this.loadDetail(this.state.selectedId, { silent: true });
+ }
+ this.setStatus("刷新完成", "success");
+ }
+
+ async loadConfigProviders() {
+ const { codexHome } = this.updateEnvironment();
+ this.state.configInfo = await getConfigProviders(codexHome);
+ this.emitChange();
+ }
+
+ async loadList(options = {}) {
+ const { dbPath } = this.updateEnvironment();
+ const keepSelection = options.keepSelection !== false;
+ const data = await listSessions(dbPath, {
+ mode: this.state.mode,
+ q: this.state.search,
+ mismatchOnly: this.state.mismatchOnly,
+ limit: 300,
+ });
+ this.state.items = Array.isArray(data.items) ? data.items : [];
+ this.state.listTotal = Number(data.total || 0);
+ this.state.mismatchCount = Number(data.mismatchCount || 0);
+
+ if (keepSelection && this.state.selectedId && this.state.items.some((item) => item.id === this.state.selectedId)) {
+ this.emitChange();
return;
- } catch {
- // fallback to panel below
}
- if (panelRef) {
- panelRef.reveal(vscode.ViewColumn.One);
+ if (this.state.items.length > 0) {
+ this.state.selectedId = this.state.items[0].id;
+ this.state.sessionHealth = null;
+ this.emitChange();
+ await this.loadDetail(this.state.selectedId, { silent: true });
return;
}
- const panel = vscode.window.createWebviewPanel(
- VIEW_TYPE,
- "Codex Session Manager",
- vscode.ViewColumn.One,
- {
- enableScripts: true,
- localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
- retainContextWhenHidden: true,
- },
- );
+ this.state.selectedId = "";
+ this.state.detail = null;
+ this.state.sessionHealth = null;
+ this.emitChange();
+ }
- panelRef = panel;
- setupWebview(panel.webview, context);
- panel.onDidDispose(() => {
- panelRef = null;
- });
- });
+ async loadDetail(id, options = {}) {
+ if (!id) {
+ return;
+ }
+ const { dbPath } = this.updateEnvironment();
+ const data = await getSessionDetail(dbPath, { id, maxMessages: 220 });
+ if (this.state.selectedId !== id) {
+ return;
+ }
+ this.state.detail = data;
+ this.emitChange();
+ if (!options.silent) {
+ this.setStatus("详情已更新", "success");
+ }
+ }
- context.subscriptions.push(openCmd);
-}
+ async selectSession(id) {
+ if (!id || id === this.state.selectedId) {
+ return;
+ }
+ this.state.selectedId = id;
+ this.state.detail = null;
+ this.state.sessionHealth = null;
+ this.emitChange();
+ try {
+ await this.loadDetail(id);
+ } catch (error) {
+ this.setStatus(`加载详情失败: ${error.message}`, "error");
+ }
+ }
-function deactivate() {}
+ async setMode(mode) {
+ const next = mode === "archive" ? "archive" : "active";
+ if (this.state.mode === next) {
+ return;
+ }
+ this.state.mode = next;
+ this.state.selectedId = "";
+ this.state.detail = null;
+ this.state.sessionHealth = null;
+ this.emitChange();
+ await this.loadList({ keepSelection: false });
+ }
-function setupWebview(webview, context) {
- webview.options = {
- enableScripts: true,
- localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, "media")],
- };
- webview.html = getWebviewHtml(webview, context.extensionUri);
+ async setSearch(search) {
+ this.state.search = String(search || "").trim();
+ this.emitChange();
+ await this.loadList({ keepSelection: false });
+ }
- webview.onDidReceiveMessage(async (msg) => {
- if (!msg || typeof msg !== "object") {
+ async toggleMismatchOnly() {
+ this.state.mismatchOnly = !this.state.mismatchOnly;
+ this.emitChange();
+ await this.loadList({ keepSelection: false });
+ }
+
+ async updateProvider(id, provider) {
+ const { dbPath } = this.updateEnvironment();
+ await updateProvider(dbPath, { id, provider });
+ await this.loadDetail(id, { silent: true });
+ await this.loadList({ keepSelection: true });
+ this.setStatus("Provider 已保存并修复可加载性", "success");
+ }
+
+ async repairProvider(id) {
+ const { dbPath } = this.updateEnvironment();
+ const data = await repairSingle(dbPath, { id });
+ await this.loadDetail(id, { silent: true });
+ await this.loadList({ keepSelection: true });
+ this.setStatus(data.changed ? `已修正不一致: ${data.from} -> ${data.to}` : "Provider 已一致,无需修正", "success");
+ }
+
+ async batchUpdateProvider(provider) {
+ const ids = this.state.items.map((item) => item.id).filter(Boolean);
+ if (!provider) {
+ throw new Error("Provider 不能为空");
+ }
+ if (!ids.length) {
+ throw new Error("当前筛选结果为空");
+ }
+ const { dbPath } = this.updateEnvironment();
+ const data = await batchUpdateProviders(dbPath, { ids, provider });
+ await this.loadList({ keepSelection: true });
+ if (this.state.selectedId) {
+ await this.loadDetail(this.state.selectedId, { silent: true });
+ }
+ const hasFailure = Number(data.failed || 0) > 0;
+ this.setStatus(`批量完成: updated=${data.updated || 0}, failed=${data.failed || 0}, missing=${data.missing || 0}`, hasFailure ? "error" : "success");
+ }
+
+ async archiveOrRestoreSelected() {
+ const session = this.state.detail?.session;
+ if (!session?.id) {
+ throw new Error("请先选择会话");
+ }
+ const { dbPath } = this.updateEnvironment();
+ const previousIndex = this.state.items.findIndex((item) => item.id === session.id);
+ if (session.archived) {
+ await restoreFromRecycle(dbPath, { id: session.id });
+ } else {
+ await moveToRecycle(dbPath, { id: session.id });
+ }
+ const currentId = session.id;
+ await this.loadList({ keepSelection: true });
+ const stillExists = this.state.items.some((item) => item.id === currentId);
+ if (stillExists) {
+ this.state.selectedId = currentId;
+ await this.loadDetail(currentId, { silent: true });
+ } else if (this.state.items.length > 0) {
+ const nextId = String(this.state.items[Math.max(0, Math.min(previousIndex, this.state.items.length - 1))]?.id || "");
+ this.state.selectedId = nextId;
+ await this.loadDetail(nextId, { silent: true });
+ } else {
+ this.state.selectedId = "";
+ this.state.detail = null;
+ this.emitChange();
+ }
+ this.setStatus(session.archived ? "会话已恢复到会话列表" : "会话已归档", "success");
+ }
+
+ async checkSessionHealth() {
+ const id = this.state.detail?.session?.id;
+ if (!id) {
+ throw new Error("请先选择会话");
+ }
+ const { dbPath } = this.updateEnvironment();
+ const data = await checkSessionHealth(dbPath, { id, maxIdleSeconds: 600 });
+ if (this.state.selectedId !== id) {
return;
}
- const id = msg.id;
- const op = msg.op;
- const payload = msg.payload || {};
- if (!id || !op) {
+ this.state.sessionHealth = data;
+ this.emitChange();
+ if (data.status === "healthy") {
+ this.setStatus("检测完成:会话状态正常", "success");
+ } else if (data.status === "running") {
+ this.setStatus("检测完成:会话仍在运行或刚刚活动", "success");
+ } else if (data.status === "stuck") {
+ this.setStatus(data.canRepair ? "检测完成:发现疑似卡住,可执行修复" : "检测完成:疑似卡住,但缺少可修复 turn_id", "error");
+ } else {
+ this.setStatus(`检测完成:${data.reason || "状态异常"}`, "error");
+ }
+ }
+
+ async repairSessionHealth() {
+ const id = this.state.detail?.session?.id;
+ if (!id) {
+ throw new Error("请先选择会话");
+ }
+ const { dbPath } = this.updateEnvironment();
+ const data = await repairSessionHealth(dbPath, { id, maxIdleSeconds: 600, reason: "interrupted" });
+ if (this.state.selectedId !== id) {
return;
}
+ this.state.sessionHealth = { id, ...(data.after || {}) };
+ await this.loadDetail(id, { silent: true });
+ this.emitChange();
+ this.setStatus(data.repaired ? `修复完成,已备份: ${data.backupPath}` : "修复执行完成,但状态仍需人工确认", data.repaired ? "success" : "error");
+ }
+}
+
+class BaseWebviewProvider {
+ constructor(context, store) {
+ this.context = context;
+ this.store = store;
+ this.view = null;
+ this.disposable = store.onDidChange(() => this.render());
+ }
+
+ dispose() {
+ this.disposable.dispose();
+ }
+
+ resolveWebviewView(webviewView) {
+ this.view = webviewView;
+ webviewView.webview.options = {
+ enableScripts: true,
+ localResourceRoots: [vscode.Uri.joinPath(this.context.extensionUri, "media")],
+ };
+ webviewView.webview.onDidReceiveMessage((msg) => this.onMessage(msg));
+ this.render();
+ }
+ render() {}
+
+ async onMessage() {}
+}
+
+class ControlsWebviewProvider extends BaseWebviewProvider {
+ render() {
+ if (!this.view) {
+ return;
+ }
+ const state = this.store.getState();
+ this.view.title = `Controls · ${state.mode === "archive" ? "Archive" : "Active"}`;
+ this.view.webview.html = renderControlsHtml(this.view.webview, state);
+ }
+
+ async onMessage(msg) {
+ if (!msg || typeof msg !== "object") {
+ return;
+ }
try {
- const data = await handleOperation(op, payload);
- webview.postMessage({ id, ok: true, data });
+ switch (msg.type) {
+ case "refresh":
+ await this.store.refreshAll({ preserveSelection: true });
+ break;
+ case "setMode":
+ await this.store.setMode(msg.mode);
+ break;
+ case "search":
+ await this.store.setSearch(msg.value);
+ break;
+ case "toggleMismatch":
+ await this.store.toggleMismatchOnly();
+ break;
+ case "batchUpdate": {
+ const provider = String(msg.provider || "").trim();
+ const count = this.store.getState().items.length;
+ const ok = await confirmAction({ message: `确认将当前筛选的 ${count} 条会话批量设置为 provider: ${provider} ?`, confirmText: "继续" });
+ if (ok.confirmed) {
+ await this.store.batchUpdateProvider(provider);
+ }
+ break;
+ }
+ default:
+ break;
+ }
} catch (error) {
- webview.postMessage({
- id,
- ok: false,
- error: error?.message || String(error),
- });
+ this.store.setStatus(error.message || String(error), "error");
}
- });
+ }
}
-function getConfig() {
- const cfg = vscode.workspace.getConfiguration("codexSessionManager");
- return {
- codexHome: String(cfg.get("codexHome") || "").trim(),
- };
+class DetailsWebviewProvider extends BaseWebviewProvider {
+ render() {
+ if (!this.view) {
+ return;
+ }
+ const state = this.store.getState();
+ this.view.title = state.detail?.session?.title ? `Details · ${state.detail.session.title}` : "Details";
+ this.view.description = state.selectedId ? shortId(state.selectedId) : "";
+ this.view.webview.html = renderDetailsHtml(this.view.webview, state);
+ }
+
+ async onMessage(msg) {
+ if (!msg || typeof msg !== "object") {
+ return;
+ }
+ const session = this.store.getState().detail?.session;
+ try {
+ switch (msg.type) {
+ case "saveProvider":
+ if (!session?.id) {
+ throw new Error("请先选择会话");
+ }
+ await this.store.updateProvider(session.id, String(msg.provider || "").trim());
+ break;
+ case "repairProvider":
+ if (!session?.id) {
+ throw new Error("请先选择会话");
+ }
+ await this.store.repairProvider(session.id);
+ break;
+ case "checkHealth":
+ await this.store.checkSessionHealth();
+ break;
+ case "repairHealth": {
+ const health = this.store.getState().sessionHealth;
+ const ok = await confirmAction({
+ message: `确认修复该会话的执行状态吗?turn_id=${health?.repairTurnId || "-"}`,
+ confirmText: "确认修复",
+ });
+ if (ok.confirmed) {
+ await this.store.repairSessionHealth();
+ }
+ break;
+ }
+ case "copyResume":
+ if (!session?.id) {
+ throw new Error("请先选择会话");
+ }
+ await copyResumeCommand({ id: session.id });
+ this.store.setStatus("Resume 命令已复制", "success");
+ break;
+ case "copySessionId":
+ if (!session?.id) {
+ throw new Error("请先选择会话");
+ }
+ await copySessionId({ id: session.id });
+ this.store.setStatus("会话 ID 已复制", "success");
+ break;
+ case "runResume":
+ if (!session?.id) {
+ throw new Error("请先选择会话");
+ }
+ await runResumeCommand({ id: session.id, cwd: session.cwd || "" });
+ this.store.setStatus("已在终端执行 Resume", "success");
+ break;
+ case "toggleArchive":
+ if (!session?.id) {
+ throw new Error("请先选择会话");
+ }
+ {
+ const ok = await confirmAction({
+ message: session.archived ? "确定将此会话恢复到会话列表吗?" : "确定将此会话归档吗?",
+ confirmText: session.archived ? "恢复会话" : "归档会话",
+ });
+ if (ok.confirmed) {
+ await this.store.archiveOrRestoreSelected();
+ }
+ }
+ break;
+ case "refreshDetail":
+ if (!session?.id) {
+ throw new Error("请先选择会话");
+ }
+ await this.store.loadDetail(session.id);
+ break;
+ default:
+ break;
+ }
+ } catch (error) {
+ this.store.setStatus(error.message || String(error), "error");
+ }
+ }
}
-function resolveCodexHome(configuredHome) {
- if (configuredHome) {
- if (configuredHome.startsWith("~")) {
- return path.join(os.homedir(), configuredHome.slice(1));
+class MessagesWebviewProvider extends BaseWebviewProvider {
+ render() {
+ if (!this.view) {
+ return;
}
- return configuredHome;
+ const state = this.store.getState();
+ const count = Number(state.detail?.messageCount || 0);
+ this.view.title = count ? `Messages · ${count}` : "Messages";
+ this.view.webview.html = renderMessagesHtml(this.view.webview, state);
}
- return path.join(os.homedir(), ".codex");
+}
+
+function webviewShell(webview, title, body) {
+ const nonce = `${Date.now()}${Math.random().toString(16).slice(2)}`;
+ const csp = ["default-src 'none'", `style-src ${webview.cspSource} 'unsafe-inline'`, `script-src 'nonce-${nonce}'`].join("; ");
+ return `
+
+
+
+
+
+ ${escapeHtml(title)}
+
+
+ ${body.replace("__NONCE__", nonce)}
+ `;
+}
+
+function renderControlsHtml(webview, state) {
+ const config = state.configInfo;
+ let configText = "Config: -";
+ if (config?.exists === false) configText = "Config: 未找到";
+ else if (config?.parseError) configText = "Config: 解析失败";
+ else if (config) configText = `Config: ${config.activeProvider || "-"}${config.providers?.length ? ` (${config.providers.length})` : ""}`;
+ const summary = state.mode === "archive"
+ ? `归档 ${state.items.length}/${state.listTotal}`
+ : state.mismatchOnly
+ ? `不一致 ${state.items.length}/${Math.max(state.mismatchCount, state.items.length)}`
+ : `会话 ${state.items.length}/${state.listTotal} · 不一致 ${state.mismatchCount}`;
+ return webviewShell(
+ webview,
+ "Controls",
+ `
+
+
+
Filters
+
+
+
+
+
+
+
${escapeHtml(state.statusText || "就绪")}
+
+
+ `,
+ );
+}
+
+function getProviderState(session) {
+ const dbProvider = String(session.provider || "").trim() || "(empty)";
+ const fileProvider = String(session.fileProvider || "").trim() || "(empty)";
+ if (session.providerMismatchError) return { text: `文件 Provider 读取失败: ${session.providerMismatchError}`, kind: "error", canRepair: false };
+ if (session.providerMismatch) return { text: `不一致: DB=${dbProvider} / FILE=${fileProvider}`, kind: "warning", canRepair: true };
+ if (session.fileProvider) return { text: `一致: FILE=${fileProvider}`, kind: "ok", canRepair: false };
+ return { text: "未读取到文件 Provider", kind: "warning", canRepair: false };
+}
+
+function getSessionHealthView(health) {
+ if (!health) return { text: "未检测", kind: "warning", canRepair: false };
+ const openCount = Number(health.openTaskCount || 0);
+ const idle = Number.isFinite(Number(health.idleSeconds)) ? `${health.idleSeconds}s` : "-";
+ if (health.status === "healthy") return { text: "正常:未发现未闭合任务", kind: "ok", canRepair: false };
+ if (health.status === "running") return { text: `运行中:未闭合任务 ${openCount} · 最近活动 ${idle} 前`, kind: "warning", canRepair: false };
+ if (health.status === "stuck") return { text: `疑似卡住:未闭合任务 ${openCount} · 空闲 ${idle}`, kind: "error", canRepair: !!health.canRepair };
+ return { text: `检测异常:${health.reason || "unknown"}`, kind: "error", canRepair: false };
+}
+
+function renderDetailsHtml(webview, state) {
+ const detail = state.detail;
+ if (!detail?.session || detail.session.id !== state.selectedId) {
+ return webviewShell(webview, "Details", `请在 Sessions 视图中选择一个会话。
`);
+ }
+ const session = detail.session;
+ const providerInfo = getProviderState(session);
+ const healthInfo = getSessionHealthView(state.sessionHealth);
+ return webviewShell(
+ webview,
+ "Details",
+ `
+
+
Session
+
${escapeHtml(session.title || session.firstUserMessage || session.id)}
+
+
+
+
Provider
+
+
${escapeHtml(providerInfo.text)}
+
+
+
+
+
+
+
Execution
+
${escapeHtml(healthInfo.text)}
+
+
+
+
+
+
+
Actions
+
+
+
+
+
+
+
+
+
+ `,
+ );
+}
+
+function renderMessagesHtml(webview, state) {
+ const detail = state.detail;
+ if (!detail?.session || detail.session.id !== state.selectedId) {
+ return webviewShell(webview, "Messages", `未选择会话。
`);
+ }
+ const stats = `消息 ${Number(detail.messageCount || 0)} · 用户 ${Number(detail.userTurns || 0)}${detail.fileError ? ` · 文件异常: ${detail.fileError}` : ""}`;
+ const messages = Array.isArray(detail.messages) ? detail.messages : [];
+ const body = messages.length
+ ? messages.map((msg) => `${escapeHtml(String(msg.role || "assistant").toUpperCase())}${escapeHtml(formatDisplayTime(msg.timestamp))}
${escapeHtml(msg.text || "")}
`).join("")
+ : `暂无可预览消息
`;
+ return webviewShell(webview, "Messages", `Summary
${escapeHtml(stats)}
${body}
`);
}
async function handleOperation(op, payload) {
@@ -1598,117 +2318,6 @@ async function runResumeCommand(payload) {
return { id, command, started: true };
}
-function getWebviewHtml(webview, extensionUri) {
- const nonce = String(Date.now()) + String(Math.random()).slice(2);
- const cssUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, "media", "webview.css"));
- const jsUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, "media", "webview.js"));
- const csp = [
- "default-src 'none'",
- `img-src ${webview.cspSource} data:`,
- `style-src ${webview.cspSource}`,
- `script-src 'nonce-${nonce}'`,
- ].join("; ");
-
- return `
-
-
-
-
-
- Codex Session Manager
-
-
-
-
-
-
-
-
-
-
- 请在左侧选择一个会话
-
-
-
-
-
-
-
- Provider
-
-
-
-
-
-
-
-
-
- 执行状态
- 未检测
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 会话内容预览
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-}
-
module.exports = {
activate,
deactivate,
diff --git a/media/webview.css b/media/webview.css
index 398d52c..67c632b 100644
--- a/media/webview.css
+++ b/media/webview.css
@@ -1,7 +1,8 @@
-:root {
+:root {
--bg: var(--vscode-editor-background, #1e1e1e);
--panel: color-mix(in srgb, var(--bg) 92%, #ffffff 8%);
--panel-2: color-mix(in srgb, var(--bg) 86%, #ffffff 14%);
+ --panel-soft: color-mix(in srgb, var(--panel) 82%, #ffffff 18%);
--line: color-mix(in srgb, var(--vscode-foreground, #d4d4d4) 18%, transparent);
--line-strong: color-mix(in srgb, var(--vscode-foreground, #d4d4d4) 28%, transparent);
--text: var(--vscode-foreground, #d4d4d4);
@@ -10,14 +11,21 @@
--danger: #cc6e67;
--warn: #c6a35b;
--ok: #59a177;
+ --radius: 10px;
}
* {
box-sizing: border-box;
}
+html,
+body {
+ height: 100%;
+}
+
body {
margin: 0;
+ overflow: hidden;
background:
radial-gradient(circle at 110% -18%, rgba(64, 124, 162, 0.22) 0%, transparent 40%),
radial-gradient(circle at -8% 0%, rgba(77, 126, 97, 0.18) 0%, transparent 38%),
@@ -30,7 +38,7 @@ body {
.app {
height: 100vh;
display: grid;
- grid-template-rows: auto 1fr auto;
+ grid-template-rows: auto minmax(0, 1fr) auto;
overflow: hidden;
}
@@ -39,7 +47,7 @@ body {
align-items: center;
justify-content: space-between;
gap: 6px;
- padding: 4px 6px;
+ padding: 6px 8px;
border-bottom: 1px solid var(--line);
background: color-mix(in srgb, var(--panel) 97%, #ffffff 3%);
}
@@ -57,8 +65,8 @@ body {
.config-provider {
border: 1px solid var(--line-strong);
- border-radius: 6px;
- padding: 4px 8px;
+ border-radius: 7px;
+ padding: 5px 8px;
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
@@ -85,40 +93,163 @@ body {
background: color-mix(in srgb, var(--danger) 22%, var(--panel));
}
-.workspace {
+.workspace-shell {
min-height: 0;
+ overflow: auto;
+ padding: 8px;
+}
+
+.workspace {
+ min-height: 100%;
display: grid;
- grid-template-columns: minmax(290px, 33%) minmax(0, 67%);
- gap: 2px;
- padding: 0;
+ grid-template-columns: minmax(300px, 34%) minmax(420px, 1fr);
+ gap: 8px;
+ align-items: stretch;
}
.sidebar,
.detail-area {
min-height: 0;
border: 1px solid var(--line);
- border-radius: 0;
- background: var(--panel);
+ border-radius: 12px;
+ background: color-mix(in srgb, var(--panel) 96%, #ffffff 4%);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
+ overflow: hidden;
}
-.sidebar {
- display: grid;
- grid-template-rows: auto auto auto auto auto 1fr;
+.panel-stack {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 6px;
+}
+
+.panel-section {
+ min-height: 44px;
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ background: color-mix(in srgb, var(--panel-soft) 90%, #ffffff 10%);
overflow: hidden;
}
+.panel-section[open] {
+ display: flex;
+ flex-direction: column;
+}
+
+.section-summary {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 42px;
+ padding: 9px 12px;
+ cursor: pointer;
+ user-select: none;
+ list-style: none;
+ font-weight: 700;
+ border-bottom: 1px solid transparent;
+}
+
+.section-summary::-webkit-details-marker {
+ display: none;
+}
+
+.section-summary::before {
+ content: "▾";
+ color: var(--muted);
+ font-size: 11px;
+ transform-origin: 50% 50%;
+ transition: transform 140ms ease;
+}
+
+.panel-section:not([open]) > .section-summary::before {
+ transform: rotate(-90deg);
+}
+
+.panel-section[open] > .section-summary {
+ border-bottom-color: var(--line);
+ background: color-mix(in srgb, var(--panel) 92%, #ffffff 8%);
+}
+
+.section-title {
+ min-width: 0;
+}
+
+.section-hint,
+.section-meta {
+ margin-left: auto;
+ min-width: 0;
+ color: var(--muted);
+ font-size: 11px;
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.section-body {
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 8px;
+}
+
+.controls-body {
+ gap: 10px;
+}
+
+.list-body,
+.message-body {
+ flex: 1;
+ padding-top: 0;
+}
+
+.detail-summary-body {
+ padding-top: 0;
+}
+
+.section-resizer {
+ position: relative;
+ flex: 0 0 10px;
+ height: 10px;
+ margin: -1px 2px;
+ cursor: row-resize;
+ border-radius: 999px;
+}
+
+.section-resizer::before {
+ content: "";
+ position: absolute;
+ left: 0;
+ right: 0;
+ top: 50%;
+ height: 2px;
+ transform: translateY(-50%);
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--line-strong) 80%, transparent);
+}
+
+.section-resizer:hover::before {
+ background: color-mix(in srgb, var(--accent) 68%, var(--line-strong));
+}
+
+body.is-resizing,
+body.is-resizing * {
+ cursor: row-resize !important;
+ user-select: none;
+}
+
.tabs {
display: grid;
grid-template-columns: 1fr 1fr;
- gap: 4px;
- padding: 4px;
- border-bottom: 1px solid var(--line);
+ gap: 6px;
}
.tab {
- height: 30px;
+ min-height: 32px;
border: 1px solid var(--line-strong);
- border-radius: 6px;
+ border-radius: 7px;
background: var(--panel-2);
color: var(--text);
font-size: 12px;
@@ -132,23 +263,7 @@ body {
background: color-mix(in srgb, var(--accent) 34%, var(--panel));
}
-.search-row,
-.filter-row,
-.batch-row,
-.list-meta {
- border-bottom: 1px solid var(--line);
-}
-
-.search-row,
-.filter-row,
-.batch-row {
- padding: 4px;
-}
-
-.search-row input {
- width: 100%;
-}
-
+.search-row input,
.filter-row .btn {
width: 100%;
}
@@ -156,36 +271,31 @@ body {
.batch-row {
display: grid;
grid-template-columns: 1fr auto;
- gap: 4px;
+ gap: 6px;
}
.batch-row button {
white-space: nowrap;
}
-.list-meta {
- padding: 4px 6px;
- color: var(--muted);
- font-size: 11px;
-}
-
.session-list {
min-height: 0;
+ height: 100%;
overflow: auto;
- padding: 4px;
display: grid;
- gap: 4px;
+ gap: 6px;
align-content: start;
+ padding-right: 2px;
}
.session-item {
width: 100%;
border: 1px solid transparent;
- border-radius: 7px;
+ border-radius: 8px;
background: color-mix(in srgb, var(--panel) 84%, #ffffff 16%);
color: var(--text);
text-align: left;
- padding: 6px;
+ padding: 7px;
display: grid;
grid-template-columns: 1fr auto;
gap: 6px;
@@ -208,7 +318,7 @@ body {
.session-title {
font-size: 12px;
font-weight: 650;
- line-height: 1.3;
+ line-height: 1.35;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -224,13 +334,13 @@ body {
}
.session-sub {
- margin-top: 1px;
+ margin-top: 2px;
color: var(--muted);
font-size: 10.5px;
}
.session-id {
- margin-top: 1px;
+ margin-top: 2px;
color: color-mix(in srgb, var(--accent) 68%, var(--muted));
font-size: 10px;
}
@@ -264,40 +374,47 @@ body {
}
.session-time {
- margin-top: 2px;
+ margin-top: 3px;
color: var(--muted);
font-size: 10px;
}
-.list-empty {
+.list-empty,
+.message-empty,
+.empty-state {
border: 1px dashed var(--line-strong);
- border-radius: 7px;
+ border-radius: 8px;
+ color: var(--muted);
+}
+
+.list-empty,
+.message-empty {
padding: 12px 8px;
text-align: center;
- color: var(--muted);
}
.detail-area {
+ display: flex;
+ flex-direction: column;
+ padding: 6px;
+}
+
+.detail-area > * {
min-height: 0;
- padding: 2px;
- overflow: hidden;
}
.empty-state {
- height: 100%;
- border: 1px dashed var(--line-strong);
- border-radius: 6px;
+ flex: 1;
display: grid;
place-items: center;
- color: var(--muted);
}
.detail-pane {
- height: 100%;
+ flex: 1;
min-height: 0;
- display: grid;
- grid-template-rows: auto auto 1fr;
- gap: 4px;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
}
.hidden {
@@ -306,34 +423,34 @@ body {
.detail-head {
border: 1px solid var(--line);
- border-radius: 7px;
+ border-radius: 8px;
background: color-mix(in srgb, var(--panel) 83%, #ffffff 17%);
- padding: 6px;
+ padding: 8px;
display: grid;
- grid-template-columns: minmax(200px, 1fr) auto;
- gap: 8px;
+ grid-template-columns: minmax(220px, 1fr) auto;
+ gap: 10px;
align-items: start;
}
.detail-title {
margin: 0;
font-size: 15px;
- line-height: 1.3;
- max-height: 40px;
- overflow: hidden;
+ line-height: 1.35;
+ max-height: 62px;
+ overflow: auto;
}
.detail-meta {
- margin-top: 4px;
+ margin-top: 6px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 4px 8px;
+ gap: 6px 10px;
}
.meta-cell {
min-width: 0;
display: grid;
- gap: 1px;
+ gap: 2px;
}
.meta-key {
@@ -359,15 +476,15 @@ body {
}
.provider-inline {
- margin-top: 5px;
+ margin-top: 6px;
border: 1px solid var(--line);
- border-radius: 6px;
+ border-radius: 7px;
background: color-mix(in srgb, var(--panel) 80%, #ffffff 20%);
- padding: 5px;
+ padding: 6px;
display: grid;
- grid-template-columns: auto auto minmax(190px, 1fr) auto auto auto auto;
+ grid-template-columns: auto auto minmax(180px, 1fr) auto auto auto auto;
align-items: center;
- gap: 5px;
+ gap: 6px;
}
.provider-label {
@@ -391,7 +508,7 @@ body {
.provider-state {
border: 1px solid var(--line-strong);
border-radius: 6px;
- padding: 3px 6px;
+ padding: 4px 6px;
color: var(--muted);
background: color-mix(in srgb, var(--panel) 74%, #ffffff 26%);
font-size: 11px;
@@ -422,20 +539,10 @@ body {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
- gap: 4px;
+ gap: 6px;
max-width: 520px;
}
-.messages-head {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- padding: 0 2px;
- font-size: 12px;
- font-weight: 650;
-}
-
.muted {
color: var(--muted);
font-size: 11px;
@@ -444,20 +551,21 @@ body {
.message-list {
min-height: 0;
+ height: 100%;
overflow: auto;
border: 1px solid var(--line);
- border-radius: 7px;
+ border-radius: 8px;
background: color-mix(in srgb, var(--panel) 80%, #ffffff 20%);
- padding: 5px;
+ padding: 6px;
display: grid;
align-content: start;
- gap: 5px;
+ gap: 6px;
}
.message {
border: 1px solid var(--line);
- border-radius: 7px;
- padding: 5px 6px;
+ border-radius: 8px;
+ padding: 6px 7px;
background: color-mix(in srgb, var(--panel) 72%, #ffffff 28%);
}
@@ -481,7 +589,7 @@ body {
align-items: center;
justify-content: space-between;
gap: 8px;
- margin-bottom: 3px;
+ margin-bottom: 4px;
}
.message-role {
@@ -503,17 +611,9 @@ body {
word-break: break-word;
}
-.message-empty {
- border: 1px dashed var(--line-strong);
- border-radius: 7px;
- padding: 11px;
- text-align: center;
- color: var(--muted);
-}
-
.status-bar {
border-top: 1px solid var(--line);
- padding: 4px 6px;
+ padding: 5px 8px;
color: var(--muted);
font-size: 11px;
background: color-mix(in srgb, var(--panel) 95%, #ffffff 5%);
@@ -532,10 +632,10 @@ body {
input,
button {
border: 1px solid var(--line-strong);
- border-radius: 6px;
+ border-radius: 7px;
background: color-mix(in srgb, var(--panel) 78%, #ffffff 22%);
color: var(--text);
- padding: 4px 8px;
+ padding: 5px 8px;
font-size: 12px;
}
@@ -571,7 +671,7 @@ button:disabled {
}
.btn.mini {
- padding: 3px 7px;
+ padding: 4px 7px;
font-size: 11.5px;
}
@@ -581,9 +681,9 @@ button:disabled {
color: #ffd9d5;
}
-@media (max-width: 1120px) {
+@media (max-width: 1180px) {
.workspace {
- grid-template-columns: 280px 1fr;
+ grid-template-columns: minmax(280px, 38%) minmax(0, 1fr);
}
.detail-head {
@@ -595,27 +695,48 @@ button:disabled {
}
}
-@media (max-width: 860px) {
+@media (max-width: 900px) {
.workspace {
grid-template-columns: 1fr;
- grid-template-rows: 280px 1fr;
+ grid-auto-rows: minmax(320px, auto);
}
- .batch-row {
- grid-template-columns: 1fr;
+ .sidebar,
+ .detail-area {
+ min-height: 360px;
}
+}
+@media (max-width: 720px) {
+ .topbar {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .top-actions {
+ width: 100%;
+ justify-content: space-between;
+ }
+
+ .config-provider {
+ max-width: none;
+ flex: 1;
+ }
+
+ .batch-row,
+ .provider-inline,
+ .exec-inline,
.detail-meta {
grid-template-columns: 1fr;
}
- .provider-inline {
- grid-template-columns: 1fr;
+ .provider-inline,
+ .exec-inline {
align-items: stretch;
}
- .config-provider {
- max-width: 180px;
+ .section-hint,
+ .section-meta {
+ max-width: 42%;
}
}
-
diff --git a/media/webview.js b/media/webview.js
index fcacc1e..79f2705 100644
--- a/media/webview.js
+++ b/media/webview.js
@@ -1,6 +1,9 @@
-(function () {
+(function () {
const vscode = acquireVsCodeApi();
-
+ const defaultUi = {
+ sections: { sidebarControls: true, sessionList: true, detailSummary: true, messagePreview: true },
+ sizes: { sidebarControls: 210, detailSummary: 280 },
+ };
const state = {
reqSeq: 1,
pending: new Map(),
@@ -17,11 +20,15 @@
providerEditing: false,
configInfo: null,
sessionHealth: null,
+ ui: buildUiState(vscode.getState()?.ui),
};
-
const els = {
configProviderInfo: document.getElementById("configProviderInfo"),
globalRefreshBtn: document.getElementById("globalRefreshBtn"),
+ sidebarStack: document.getElementById("sidebarStack"),
+ sidebarControlsSection: document.getElementById("sidebarControlsSection"),
+ sessionListSection: document.getElementById("sessionListSection"),
+ sidebarResizer: document.getElementById("sidebarResizer"),
tabActiveBtn: document.getElementById("tabActiveBtn"),
tabRecycleBtn: document.getElementById("tabRecycleBtn"),
searchInput: document.getElementById("searchInput"),
@@ -32,6 +39,9 @@
sessionList: document.getElementById("sessionList"),
emptyState: document.getElementById("emptyState"),
detailPane: document.getElementById("detailPane"),
+ detailSummarySection: document.getElementById("detailSummarySection"),
+ messageSection: document.getElementById("messageSection"),
+ detailResizer: document.getElementById("detailResizer"),
detailTitle: document.getElementById("detailTitle"),
detailMeta: document.getElementById("detailMeta"),
providerInline: document.getElementById("providerInline"),
@@ -56,59 +66,54 @@
statusBar: document.getElementById("statusBar"),
};
+ function clamp(value, min, max) {
+ return Math.min(Math.max(value, min), max);
+ }
+ function buildUiState(raw) {
+ return {
+ sections: {
+ sidebarControls: raw?.sections?.sidebarControls !== false,
+ sessionList: raw?.sections?.sessionList !== false,
+ detailSummary: raw?.sections?.detailSummary !== false,
+ messagePreview: raw?.sections?.messagePreview !== false,
+ },
+ sizes: {
+ sidebarControls: clamp(Number(raw?.sizes?.sidebarControls) || defaultUi.sizes.sidebarControls, 150, 480),
+ detailSummary: clamp(Number(raw?.sizes?.detailSummary) || defaultUi.sizes.detailSummary, 180, 640),
+ },
+ };
+ }
+ function persistUiState() {
+ vscode.setState({ ui: state.ui });
+ }
function esc(text) {
- return String(text ?? "").replace(/[&<>"']/g, (ch) => {
- const map = {
- "&": "&",
- "<": "<",
- ">": ">",
- '"': """,
- "'": "'",
- };
- return map[ch] || ch;
- });
+ return String(text ?? "").replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[ch] || ch));
}
-
function escWithBreaks(text) {
return esc(text).replace(/\n/g, "
");
}
-
function shortId(id) {
const raw = String(id || "");
return raw.length > 14 ? `${raw.slice(0, 8)}...${raw.slice(-4)}` : raw;
}
-
function formatTime(value) {
- if (!value) {
- return "-";
- }
+ if (!value) return "-";
const date = new Date(value);
- if (Number.isNaN(date.getTime())) {
- return String(value);
- }
- return date.toLocaleString();
+ return Number.isNaN(date.getTime()) ? String(value) : date.toLocaleString();
}
-
function setStatus(text, type = "info") {
els.statusBar.textContent = text;
els.statusBar.classList.remove("is-error", "is-success");
- if (type === "error") {
- els.statusBar.classList.add("is-error");
- }
- if (type === "success") {
- els.statusBar.classList.add("is-success");
- }
+ if (type === "error") els.statusBar.classList.add("is-error");
+ if (type === "success") els.statusBar.classList.add("is-success");
}
-
function rpc(op, payload) {
return new Promise((resolve, reject) => {
const id = String(state.reqSeq++);
state.pending.set(id, { resolve, reject });
vscode.postMessage({ id, op, payload: payload || {} });
setTimeout(() => {
- if (!state.pending.has(id)) {
- return;
- }
+ if (!state.pending.has(id)) return;
state.pending.delete(id);
reject(new Error(`请求超时: ${op}`));
}, 45000);
@@ -123,17 +128,11 @@
return false;
}
}
-
window.addEventListener("message", (event) => {
const msg = event.data;
- if (!msg || !msg.id) {
- return;
- }
+ if (!msg?.id) return;
const task = state.pending.get(msg.id);
- if (!task) {
- return;
- }
-
+ if (!task) return;
state.pending.delete(msg.id);
if (msg.ok) {
task.resolve(msg.data);
@@ -142,148 +141,162 @@
task.reject(new Error(msg.error || "未知错误"));
});
+ function getSummaryHeight(section) {
+ return Math.ceil(section.querySelector("summary")?.getBoundingClientRect().height || 42);
+ }
+ function getSplitBounds(stack, top, bottom, handle, options = {}) {
+ const stackHeight = Math.ceil(stack.getBoundingClientRect().height || stack.clientHeight || 0);
+ const handleHeight = Math.ceil(handle.getBoundingClientRect().height || handle.offsetHeight || 10);
+ const min = getSummaryHeight(top) + Number(options.minTopBody || 110);
+ const rawMax = stackHeight - handleHeight - getSummaryHeight(bottom) - Number(options.minBottomBody || 140);
+ return { min, max: Math.max(min, rawMax) };
+ }
+ function syncSplitLayout(stack, top, bottom, handle, sizeKey, options = {}) {
+ const topOpen = !!top.open;
+ const bottomOpen = !!bottom.open;
+ top.style.height = "";
+ top.style.flex = "0 0 auto";
+ bottom.style.flex = "0 0 auto";
+ if (topOpen && bottomOpen) {
+ const bounds = getSplitBounds(stack, top, bottom, handle, options);
+ const next = clamp(Math.round(state.ui.sizes[sizeKey] || bounds.min), bounds.min, bounds.max);
+ state.ui.sizes[sizeKey] = next;
+ top.style.height = `${next}px`;
+ bottom.style.flex = "1 1 0";
+ handle.classList.remove("hidden");
+ return;
+ }
+ handle.classList.add("hidden");
+ if (topOpen) top.style.flex = "1 1 0";
+ else if (bottomOpen) bottom.style.flex = "1 1 0";
+ }
+ function syncSectionLayout() {
+ syncSplitLayout(els.sidebarStack, els.sidebarControlsSection, els.sessionListSection, els.sidebarResizer, "sidebarControls", { minTopBody: 130, minBottomBody: 140 });
+ syncSplitLayout(els.detailPane, els.detailSummarySection, els.messageSection, els.detailResizer, "detailSummary", { minTopBody: 180, minBottomBody: 180 });
+ }
+ function bindSectionToggle(section, key) {
+ section.addEventListener("toggle", () => {
+ state.ui.sections[key] = !!section.open;
+ syncSectionLayout();
+ persistUiState();
+ });
+ }
+ function bindSplitResizer(stack, top, bottom, handle, sizeKey, options = {}) {
+ handle.addEventListener("pointerdown", (event) => {
+ if (!top.open || !bottom.open) return;
+ const startHeight = Math.round(top.getBoundingClientRect().height);
+ const startY = event.clientY;
+ const bounds = getSplitBounds(stack, top, bottom, handle, options);
+ event.preventDefault();
+ document.body.classList.add("is-resizing");
+ const onMove = (moveEvent) => {
+ state.ui.sizes[sizeKey] = clamp(startHeight + (moveEvent.clientY - startY), bounds.min, bounds.max);
+ syncSectionLayout();
+ };
+ const stop = () => {
+ document.body.classList.remove("is-resizing");
+ window.removeEventListener("pointermove", onMove);
+ window.removeEventListener("pointerup", stop);
+ window.removeEventListener("pointercancel", stop);
+ persistUiState();
+ };
+ window.addEventListener("pointermove", onMove);
+ window.addEventListener("pointerup", stop);
+ window.addEventListener("pointercancel", stop);
+ });
+ }
+ function initLayout() {
+ els.sidebarControlsSection.open = state.ui.sections.sidebarControls;
+ els.sessionListSection.open = state.ui.sections.sessionList;
+ els.detailSummarySection.open = state.ui.sections.detailSummary;
+ els.messageSection.open = state.ui.sections.messagePreview;
+ bindSectionToggle(els.sidebarControlsSection, "sidebarControls");
+ bindSectionToggle(els.sessionListSection, "sessionList");
+ bindSectionToggle(els.detailSummarySection, "detailSummary");
+ bindSectionToggle(els.messageSection, "messagePreview");
+ bindSplitResizer(els.sidebarStack, els.sidebarControlsSection, els.sessionListSection, els.sidebarResizer, "sidebarControls", { minTopBody: 130, minBottomBody: 140 });
+ bindSplitResizer(els.detailPane, els.detailSummarySection, els.messageSection, els.detailResizer, "detailSummary", { minTopBody: 180, minBottomBody: 180 });
+ window.addEventListener("resize", syncSectionLayout);
+ syncSectionLayout();
+ }
+
function updateTabs() {
const isArchive = state.mode === "archive";
els.tabActiveBtn.classList.toggle("is-active", !isArchive);
els.tabRecycleBtn.classList.toggle("is-active", isArchive);
}
-
function renderConfigProvider() {
const data = state.configInfo;
els.configProviderInfo.classList.remove("is-error", "is-warning", "is-ok");
-
if (!data) {
els.configProviderInfo.textContent = "Config Provider: -";
els.configProviderInfo.title = "";
return;
}
-
if (!data.exists) {
els.configProviderInfo.textContent = "Config: 未找到";
els.configProviderInfo.classList.add("is-warning");
els.configProviderInfo.title = data.configPath || "";
return;
}
-
if (data.parseError) {
els.configProviderInfo.textContent = "Config: 解析失败";
els.configProviderInfo.classList.add("is-error");
els.configProviderInfo.title = `${data.configPath || ""}\n${data.parseError}`;
return;
}
-
const active = data.activeProvider || "-";
const count = Array.isArray(data.providers) ? data.providers.length : 0;
els.configProviderInfo.textContent = `Config: ${active}${count ? ` (${count})` : ""}`;
els.configProviderInfo.classList.add("is-ok");
els.configProviderInfo.title = `${data.configPath || ""}\nProviders: ${(data.providers || []).join(", ") || "-"}`;
}
-
function updateMismatchToggle() {
const active = state.mismatchOnly;
els.mismatchOnlyBtn.classList.toggle("is-active", active);
els.mismatchOnlyBtn.setAttribute("aria-pressed", active ? "true" : "false");
els.mismatchOnlyBtn.textContent = active ? "仅看不一致: 开" : "仅看不一致";
}
-
function renderList() {
if (state.loadingList) {
- els.sessionList.innerHTML = "\u6b63\u5728\u52a0\u8f7d...
";
+ els.sessionList.innerHTML = '正在加载...
';
return;
}
-
if (!state.items.length) {
- if (state.mode === "archive") {
- els.sessionList.innerHTML = "\u5f52\u6863\u5217\u8868\u4e3a\u7a7a
";
- } else if (state.mismatchOnly) {
- els.sessionList.innerHTML = "\u5f53\u524d\u7b5b\u9009\u4e0b\u6ca1\u6709\u4e0d\u4e00\u81f4\u4f1a\u8bdd
";
- } else {
- els.sessionList.innerHTML = "\u6ca1\u6709\u5339\u914d\u4f1a\u8bdd
";
+ if (state.mode === "archive") els.sessionList.innerHTML = '归档列表为空
';
+ else if (state.mismatchOnly) els.sessionList.innerHTML = '当前筛选下没有不一致会话
';
+ else els.sessionList.innerHTML = '没有匹配会话
';
+ return;
+ }
+ els.sessionList.innerHTML = state.items.map((item) => {
+ const selected = item.id === state.selectedId ? "is-selected" : "";
+ const line2 = item.firstUserMessage || item.cwd || "无会话摘要";
+ const provider = item.provider || "(empty)";
+ const fileProvider = item.fileProvider || "(empty)";
+ let providerHtml = `${esc(provider)}
`;
+ if (item.providerMismatch) {
+ providerHtml = `DB:${esc(provider)}FILE:${esc(fileProvider)}
`;
}
- return;
- }
-
- els.sessionList.innerHTML = state.items
- .map((item) => {
- const selected = item.id === state.selectedId ? "is-selected" : "";
- const line2 = item.firstUserMessage || item.cwd || "\u65e0\u4f1a\u8bdd\u6458\u8981";
- const provider = item.provider || "(empty)";
- const fileProvider = item.fileProvider || "(empty)";
-
- let providerHtml = `${esc(provider)}
`;
- if (item.providerMismatch) {
- providerHtml = `
-
- DB:${esc(provider)}
- FILE:${esc(fileProvider)}
-
- `;
- }
-
- return `
-
- `;
- })
- .join("");
+ return ``;
+ }).join("");
}
-
-
function renderMeta(session) {
- const chunks = [
+ return [
`ID${esc(session.id)}
`,
`Source${esc(session.source || "-")}
`,
`更新${esc(formatTime(session.updatedAt))}
`,
`创建${esc(formatTime(session.createdAt))}
`,
`CWD${esc(session.cwd || "-")}
`,
- ];
- return chunks.join("");
+ ].join("");
}
-
function getProviderState(session) {
const dbProvider = String(session.provider || "").trim() || "(empty)";
const fileProvider = String(session.fileProvider || "").trim() || "(empty)";
-
- if (session.providerMismatchError) {
- return {
- text: `文件 Provider 读取失败: ${session.providerMismatchError}`,
- kind: "error",
- canRepair: false,
- };
- }
-
- if (session.providerMismatch) {
- return {
- text: `不一致: DB=${dbProvider} / FILE=${fileProvider}`,
- kind: "warning",
- canRepair: true,
- };
- }
-
- if (session.fileProvider) {
- return {
- text: `一致: FILE=${fileProvider}`,
- kind: "ok",
- canRepair: false,
- };
- }
-
- return {
- text: "未读取到文件 Provider",
- kind: "warning",
- canRepair: false,
- };
+ if (session.providerMismatchError) return { text: `文件 Provider 读取失败: ${session.providerMismatchError}`, kind: "error", canRepair: false };
+ if (session.providerMismatch) return { text: `不一致: DB=${dbProvider} / FILE=${fileProvider}`, kind: "warning", canRepair: true };
+ if (session.fileProvider) return { text: `一致: FILE=${fileProvider}`, kind: "ok", canRepair: false };
+ return { text: "未读取到文件 Provider", kind: "warning", canRepair: false };
}
-
function setProviderEditing(editing) {
state.providerEditing = !!editing;
els.providerValue.classList.toggle("hidden", state.providerEditing);
@@ -291,102 +304,53 @@
els.providerEditInput.classList.toggle("hidden", !state.providerEditing);
els.saveProviderBtn.classList.toggle("hidden", !state.providerEditing);
els.cancelProviderBtn.classList.toggle("hidden", !state.providerEditing);
-
const canRepair = els.repairProviderBtn.dataset.canRepair === "1";
els.repairProviderBtn.classList.toggle("hidden", !canRepair || state.providerEditing);
-
if (state.providerEditing) {
els.providerEditInput.focus();
els.providerEditInput.select();
}
}
-
function renderProviderInline(session) {
els.providerInline.classList.remove("hidden");
els.providerValue.textContent = session.provider || "(empty)";
els.providerEditInput.value = session.provider || "";
-
const info = getProviderState(session);
els.providerState.textContent = info.text;
els.providerState.classList.remove("is-ok", "is-warning", "is-error");
- if (info.kind === "ok") {
- els.providerState.classList.add("is-ok");
- } else if (info.kind === "warning") {
- els.providerState.classList.add("is-warning");
- } else if (info.kind === "error") {
- els.providerState.classList.add("is-error");
- }
+ if (info.kind === "ok") els.providerState.classList.add("is-ok");
+ else if (info.kind === "warning") els.providerState.classList.add("is-warning");
+ else if (info.kind === "error") els.providerState.classList.add("is-error");
els.repairProviderBtn.dataset.canRepair = info.canRepair ? "1" : "0";
setProviderEditing(false);
}
-
function getSessionHealthView(health) {
- if (!health) {
- return {
- text: "未检测",
- kind: "warning",
- canRepair: false,
- };
- }
-
+ if (!health) return { text: "未检测", kind: "warning", canRepair: false };
const openCount = Number(health.openTaskCount || 0);
const idle = Number.isFinite(Number(health.idleSeconds)) ? `${health.idleSeconds}s` : "-";
-
- if (health.status === "healthy") {
- return {
- text: "正常:未发现未闭合任务",
- kind: "ok",
- canRepair: false,
- };
- }
-
- if (health.status === "running") {
- return {
- text: `运行中:未闭合任务 ${openCount} · 最近活动 ${idle} 前`,
- kind: "warning",
- canRepair: false,
- };
- }
-
- if (health.status === "stuck") {
- return {
- text: `疑似卡住:未闭合任务 ${openCount} · 空闲 ${idle}`,
- kind: "error",
- canRepair: !!health.canRepair,
- };
- }
-
- return {
- text: `检测异常:${health.reason || "unknown"}`,
- kind: "error",
- canRepair: false,
- };
+ if (health.status === "healthy") return { text: "正常:未发现未闭合任务", kind: "ok", canRepair: false };
+ if (health.status === "running") return { text: `运行中:未闭合任务 ${openCount} · 最近活动 ${idle} 前`, kind: "warning", canRepair: false };
+ if (health.status === "stuck") return { text: `疑似卡住:未闭合任务 ${openCount} · 空闲 ${idle}`, kind: "error", canRepair: !!health.canRepair };
+ return { text: `检测异常:${health.reason || "unknown"}`, kind: "error", canRepair: false };
}
-
function renderSessionHealth() {
const session = state.detail?.session;
- if (!session || !session.id) {
+ if (!session?.id) {
els.execInline.classList.add("hidden");
return;
}
-
els.execInline.classList.remove("hidden");
const info = getSessionHealthView(state.sessionHealth);
els.execStateText.textContent = info.text;
els.execStateText.classList.remove("is-ok", "is-warning", "is-error");
- if (info.kind === "ok") {
- els.execStateText.classList.add("is-ok");
- } else if (info.kind === "warning") {
- els.execStateText.classList.add("is-warning");
- } else {
- els.execStateText.classList.add("is-error");
- }
-
+ if (info.kind === "ok") els.execStateText.classList.add("is-ok");
+ else if (info.kind === "warning") els.execStateText.classList.add("is-warning");
+ else els.execStateText.classList.add("is-error");
els.repairExecBtn.classList.toggle("hidden", !info.canRepair);
}
function renderDetail() {
const detail = state.detail;
- if (!detail || !detail.session || detail.session.id !== state.selectedId) {
+ if (!detail?.session || detail.session.id !== state.selectedId) {
els.emptyState.classList.remove("hidden");
els.detailPane.classList.add("hidden");
els.detailTitle.textContent = "";
@@ -395,93 +359,64 @@
els.execInline.classList.add("hidden");
els.messageStats.textContent = "";
els.messageList.innerHTML = "";
+ syncSectionLayout();
return;
}
-
const session = detail.session;
els.emptyState.classList.add("hidden");
els.detailPane.classList.remove("hidden");
-
els.detailTitle.textContent = session.title || session.firstUserMessage || session.id;
els.detailMeta.innerHTML = renderMeta(session);
renderProviderInline(session);
renderSessionHealth();
-
- els.deleteRestoreBtn.textContent = session.archived ? "\u6062\u590d\u4f1a\u8bdd" : "\u5f52\u6863\u4f1a\u8bdd";
+ els.deleteRestoreBtn.textContent = session.archived ? "恢复会话" : "归档会话";
els.deleteRestoreBtn.classList.toggle("danger", !session.archived);
-
const msgCount = Number(detail.messageCount || 0);
const userTurns = Number(detail.userTurns || 0);
let stats = `消息 ${msgCount} · 用户 ${userTurns}`;
- if (detail.fileError) {
- stats += ` · 文件异常: ${detail.fileError}`;
- }
+ if (detail.fileError) stats += ` · 文件异常: ${detail.fileError}`;
els.messageStats.textContent = stats;
-
const messages = Array.isArray(detail.messages) ? detail.messages : [];
if (!messages.length) {
els.messageList.innerHTML = '暂无可预览消息
';
+ syncSectionLayout();
return;
}
-
- els.messageList.innerHTML = messages
- .map((msg) => {
- const role = String(msg.role || "assistant").toLowerCase();
- const roleClass = role === "user" ? "role-user" : role === "system" ? "role-system" : "role-assistant";
- return `
-
-
-
${esc(role)}
-
${esc(formatTime(msg.timestamp))}
-
- ${escWithBreaks(msg.text || "")}
-
- `;
- })
- .join("");
-
+ els.messageList.innerHTML = messages.map((msg) => {
+ const role = String(msg.role || "assistant").toLowerCase();
+ const roleClass = role === "user" ? "role-user" : role === "system" ? "role-system" : "role-assistant";
+ return `${esc(role)}
${esc(formatTime(msg.timestamp))}
${escWithBreaks(msg.text || "")}
`;
+ }).join("");
els.messageList.scrollTop = 0;
+ syncSectionLayout();
}
-
function renderSummary(total) {
const totalNum = Number(total || state.items.length);
if (state.mode === "archive") {
- els.listSummary.textContent = `\u5f52\u6863 ${state.items.length}/${totalNum}`;
+ els.listSummary.textContent = `归档 ${state.items.length}/${totalNum}`;
return;
}
-
if (state.mismatchOnly) {
- els.listSummary.textContent = `\u4e0d\u4e00\u81f4 ${state.items.length}/${Math.max(state.mismatchCount, state.items.length)}`;
+ els.listSummary.textContent = `不一致 ${state.items.length}/${Math.max(state.mismatchCount, state.items.length)}`;
return;
}
-
- els.listSummary.textContent = `\u4f1a\u8bdd ${state.items.length}/${totalNum} \u00b7 \u4e0d\u4e00\u81f4 ${state.mismatchCount}`;
+ els.listSummary.textContent = `会话 ${state.items.length}/${totalNum} · 不一致 ${state.mismatchCount}`;
}
-
-
function setSelected(id) {
state.selectedId = id;
renderList();
}
-
function captureListContext() {
const hasSearchFocus = document.activeElement === els.searchInput;
- const selectionStart = hasSearchFocus ? els.searchInput.selectionStart : null;
- const selectionEnd = hasSearchFocus ? els.searchInput.selectionEnd : null;
-
return {
hasSearchFocus,
- selectionStart,
- selectionEnd,
+ selectionStart: hasSearchFocus ? els.searchInput.selectionStart : null,
+ selectionEnd: hasSearchFocus ? els.searchInput.selectionEnd : null,
listScrollTop: Number(els.sessionList.scrollTop || 0),
};
}
-
function restoreListContext(ctx) {
- if (!ctx) {
- return;
- }
-
+ if (!ctx) return;
if (ctx.hasSearchFocus) {
els.searchInput.focus();
if (Number.isInteger(ctx.selectionStart) && Number.isInteger(ctx.selectionEnd)) {
@@ -492,20 +427,13 @@
}
}
}
-
- if (Number.isFinite(ctx.listScrollTop)) {
- els.sessionList.scrollTop = ctx.listScrollTop;
- }
+ if (Number.isFinite(ctx.listScrollTop)) els.sessionList.scrollTop = ctx.listScrollTop;
}
-
function chooseNearbySessionId(fallbackIndex) {
- if (!state.items.length) {
- return "";
- }
+ if (!state.items.length) return "";
const safeIndex = Math.max(0, Math.min(Number(fallbackIndex || 0), state.items.length - 1));
return String(state.items[safeIndex]?.id || state.items[0].id || "");
}
-
async function loadHealth() {
const data = await rpc("health");
if (!data.exists) {
@@ -514,40 +442,26 @@
}
setStatus(`就绪 · ${data.codexHome}`, "success");
}
-
async function loadConfigProviders() {
state.configInfo = await rpc("getConfigProviders");
renderConfigProvider();
}
-
async function loadList(options = {}) {
const keepSelection = options.keepSelection !== false;
const silent = options.silent === true;
-
state.loadingList = true;
renderList();
- if (!silent) {
- setStatus("加载会话列表...");
- }
-
- const data = await rpc("listSessions", {
- mode: state.mode,
- q: state.search,
- mismatchOnly: state.mismatchOnly,
- limit: 300,
- });
-
+ if (!silent) setStatus("加载会话列表...");
+ const data = await rpc("listSessions", { mode: state.mode, q: state.search, mismatchOnly: state.mismatchOnly, limit: 300 });
state.loadingList = false;
state.items = Array.isArray(data.items) ? data.items : [];
state.listTotal = Number(data.total || 0);
state.mismatchCount = Number(data.mismatchCount || 0);
renderSummary(state.listTotal);
-
if (keepSelection && state.selectedId && state.items.some((item) => item.id === state.selectedId)) {
renderList();
return;
}
-
if (state.items.length > 0) {
state.sessionHealth = null;
setSelected(state.items[0].id);
@@ -559,78 +473,51 @@
renderList();
renderDetail();
}
-
- if (!silent) {
- setStatus("列表已更新", "success");
- }
+ if (!silent) setStatus("列表已更新", "success");
}
-
async function loadDetail(id, options = {}) {
+ if (!id) return;
const silent = options.silent === true;
- if (!id) {
- return;
- }
-
- if (!silent) {
- setStatus(`加载会话 ${shortId(id)} 详情...`);
- }
-
+ if (!silent) setStatus(`加载会话 ${shortId(id)} 详情...`);
const data = await rpc("getSessionDetail", { id, maxMessages: 220 });
- if (state.selectedId !== id) {
- return;
- }
-
+ if (state.selectedId !== id) return;
state.detail = data;
renderDetail();
-
- if (!silent) {
- setStatus("详情已更新", "success");
- }
+ if (!silent) setStatus("详情已更新", "success");
}
-
async function onRefreshAll() {
try {
await loadConfigProviders();
await loadList({ keepSelection: true });
- if (state.selectedId) {
- await loadDetail(state.selectedId, { silent: true });
- }
+ if (state.selectedId) await loadDetail(state.selectedId, { silent: true });
setStatus("刷新完成", "success");
} catch (error) {
setStatus(`刷新失败: ${error.message}`, "error");
}
}
-
async function onSelectSession(id) {
- if (!id || id === state.selectedId) {
- return;
- }
-
+ if (!id || id === state.selectedId) return;
setSelected(id);
state.detail = null;
state.sessionHealth = null;
renderDetail();
-
try {
await loadDetail(id);
} catch (error) {
setStatus(`加载详情失败: ${error.message}`, "error");
}
}
-
async function onSaveProvider() {
const id = state.detail?.session?.id;
if (!id) {
setStatus("请先选择会话", "error");
return;
}
-
const provider = els.providerEditInput.value.trim();
if (!provider) {
setStatus("Provider 不能为空", "error");
return;
}
-
els.saveProviderBtn.disabled = true;
try {
setStatus("正在保存 Provider...");
@@ -645,115 +532,72 @@
els.saveProviderBtn.disabled = false;
}
}
-
async function onRepairProvider() {
const id = state.detail?.session?.id;
if (!id) {
setStatus("请先选择会话", "error");
return;
}
-
els.repairProviderBtn.disabled = true;
try {
const data = await rpc("repairSingle", { id });
await loadDetail(id, { silent: true });
await loadList({ keepSelection: true, silent: true });
- if (data.changed) {
- setStatus(`已修正不一致: ${data.from} -> ${data.to}`, "success");
- } else {
- setStatus("Provider 已一致,无需修正", "success");
- }
+ setStatus(data.changed ? `已修正不一致: ${data.from} -> ${data.to}` : "Provider 已一致,无需修正", "success");
} catch (error) {
setStatus(`修正失败: ${error.message}`, "error");
} finally {
els.repairProviderBtn.disabled = false;
}
}
-
async function onBatchUpdateProvider() {
const provider = els.batchProviderInput.value.trim();
if (!provider) {
setStatus("请填写批量 Provider", "error");
return;
}
-
const ids = state.items.map((item) => item.id).filter(Boolean);
if (!ids.length) {
setStatus("当前筛选结果为空", "error");
return;
}
- const loadedHint =
- state.listTotal > ids.length
- ? `\n\u6ce8\u610f\uff1a\u5f53\u524d\u7b5b\u9009\u603b\u6570\u4e3a ${state.listTotal}\uff0c\u672c\u6b21\u4ec5\u4fee\u6539\u5df2\u52a0\u8f7d\u7684 ${ids.length} \u6761\uff08\u5217\u8868\u4e0a\u9650 300\uff09\u3002`
- : "";
-
- const ok = await confirmDanger(`\u786e\u8ba4\u5c06\u5f53\u524d\u7b5b\u9009\u7684 ${ids.length} \u6761\u4f1a\u8bdd\u6279\u91cf\u8bbe\u7f6e\u4e3a provider: ${provider} ?${loadedHint}`, "\u7ee7\u7eed");
- if (!ok) {
- return;
- }
- const ok2 = await confirmDanger("\u8be5\u64cd\u4f5c\u4f1a\u540c\u65f6\u5199\u5165\u6570\u636e\u5e93\u548c\u4f1a\u8bdd\u6587\u4ef6\uff0c\u662f\u5426\u7ee7\u7eed\uff1f", "\u786e\u8ba4\u6279\u91cf\u4fee\u6539");
- if (!ok2) {
- return;
- }
-
+ const loadedHint = state.listTotal > ids.length ? `\n注意:当前筛选总数为 ${state.listTotal},本次仅修改已加载的 ${ids.length} 条(列表上限 300)。` : "";
+ const ok = await confirmDanger(`确认将当前筛选的 ${ids.length} 条会话批量设置为 provider: ${provider} ?${loadedHint}`, "继续");
+ if (!ok) return;
+ const ok2 = await confirmDanger("该操作会同时写入数据库和会话文件,是否继续?", "确认批量修改");
+ if (!ok2) return;
els.batchUpdateBtn.disabled = true;
try {
setStatus(`批量更新中 (${ids.length})...`);
const data = await rpc("batchUpdate", { ids, provider });
await loadList({ keepSelection: true, silent: true });
- if (state.selectedId) {
- await loadDetail(state.selectedId, { silent: true });
- }
-
+ if (state.selectedId) await loadDetail(state.selectedId, { silent: true });
const hasFailure = Number(data.failed || 0) > 0;
- const summary = `批量完成: updated=${data.updated || 0}, failed=${data.failed || 0}, missing=${data.missing || 0}`;
- setStatus(summary, hasFailure ? "error" : "success");
+ setStatus(`批量完成: updated=${data.updated || 0}, failed=${data.failed || 0}, missing=${data.missing || 0}`, hasFailure ? "error" : "success");
} catch (error) {
setStatus(`批量更新失败: ${error.message}`, "error");
} finally {
els.batchUpdateBtn.disabled = false;
}
}
-
async function onDeleteOrRestore() {
const session = state.detail?.session;
- if (!session || !session.id) {
- setStatus("\u8bf7\u5148\u9009\u62e9\u4f1a\u8bdd", "error");
+ if (!session?.id) {
+ setStatus("请先选择会话", "error");
return;
}
-
const uiContext = captureListContext();
const previousIndex = state.items.findIndex((item) => item.id === session.id);
-
const isArchived = !!session.archived;
- if (!isArchived) {
- const ok = await confirmDanger("\u786e\u5b9a\u5c06\u6b64\u4f1a\u8bdd\u5f52\u6863\u5417\uff1f", "\u5f52\u6863\u4f1a\u8bdd");
- if (!ok) {
- return;
- }
- } else {
- const ok = await confirmDanger("\u786e\u5b9a\u5c06\u6b64\u4f1a\u8bdd\u6062\u590d\u5230\u4f1a\u8bdd\u5217\u8868\u5417\uff1f", "\u6062\u590d\u4f1a\u8bdd");
- if (!ok) {
- return;
- }
- }
-
+ const ok = await confirmDanger(isArchived ? "确定将此会话恢复到会话列表吗?" : "确定将此会话归档吗?", isArchived ? "恢复会话" : "归档会话");
+ if (!ok) return;
els.deleteRestoreBtn.disabled = true;
try {
- const actionData = isArchived
- ? await rpc("restoreFromRecycle", { id: session.id })
- : await rpc("moveToRecycle", { id: session.id });
-
- if (!isArchived && actionData && actionData.moved === false) {
- setStatus(actionData.alreadyInRecycle ? "\u4f1a\u8bdd\u5df2\u5728\u5f52\u6863\u5217\u8868" : "\u5f52\u6863\u672a\u751f\u6548\uff0c\u8bf7\u5237\u65b0\u540e\u91cd\u8bd5", "error");
- }
- if (isArchived && actionData && actionData.restored === false) {
- setStatus(actionData.alreadyActive ? "\u4f1a\u8bdd\u5df2\u5728\u4f1a\u8bdd\u5217\u8868" : "\u6062\u590d\u672a\u751f\u6548\uff0c\u8bf7\u5237\u65b0\u540e\u91cd\u8bd5", "error");
- }
-
+ const actionData = isArchived ? await rpc("restoreFromRecycle", { id: session.id }) : await rpc("moveToRecycle", { id: session.id });
+ if (!isArchived && actionData?.moved === false) setStatus(actionData.alreadyInRecycle ? "会话已在归档列表" : "归档未生效,请刷新后重试", "error");
+ if (isArchived && actionData?.restored === false) setStatus(actionData.alreadyActive ? "会话已在会话列表" : "恢复未生效,请刷新后重试", "error");
const currentId = session.id;
await loadList({ keepSelection: true, silent: true });
-
const stillExists = state.items.some((item) => item.id === currentId);
if (stillExists) {
setSelected(currentId);
@@ -767,60 +611,43 @@
state.detail = null;
renderDetail();
}
-
- if ((isArchived && actionData?.restored) || (!isArchived && actionData?.moved)) {
- setStatus(isArchived ? "\u4f1a\u8bdd\u5df2\u6062\u590d\u5230\u4f1a\u8bdd\u5217\u8868" : "\u4f1a\u8bdd\u5df2\u5f52\u6863", "success");
- }
+ if ((isArchived && actionData?.restored) || (!isArchived && actionData?.moved)) setStatus(isArchived ? "会话已恢复到会话列表" : "会话已归档", "success");
} catch (error) {
- setStatus(`\u64cd\u4f5c\u5931\u8d25: ${error.message}`, "error");
+ setStatus(`操作失败: ${error.message}`, "error");
} finally {
els.deleteRestoreBtn.disabled = false;
restoreListContext(uiContext);
}
}
-
-
async function onCheckSessionHealth() {
const id = state.detail?.session?.id;
if (!id) {
setStatus("请先选择会话", "error");
return;
}
-
els.checkExecBtn.disabled = true;
try {
setStatus("正在检测会话执行状态...");
const data = await rpc("checkSessionHealth", { id, maxIdleSeconds: 600 });
- if (state.selectedId !== id) {
- return;
- }
-
+ if (state.selectedId !== id) return;
state.sessionHealth = data;
renderSessionHealth();
-
- if (data.status === "healthy") {
- setStatus("检测完成:会话状态正常", "success");
- } else if (data.status === "running") {
- setStatus("检测完成:会话仍在运行或刚刚活动", "success");
- } else if (data.status === "stuck") {
- setStatus(data.canRepair ? "检测完成:发现疑似卡住,可执行修复" : "检测完成:疑似卡住,但缺少可修复 turn_id", "error");
- } else {
- setStatus(`检测完成:${data.reason || "状态异常"}`, "error");
- }
+ if (data.status === "healthy") setStatus("检测完成:会话状态正常", "success");
+ else if (data.status === "running") setStatus("检测完成:会话仍在运行或刚刚活动", "success");
+ else if (data.status === "stuck") setStatus(data.canRepair ? "检测完成:发现疑似卡住,可执行修复" : "检测完成:疑似卡住,但缺少可修复 turn_id", "error");
+ else setStatus(`检测完成:${data.reason || "状态异常"}`, "error");
} catch (error) {
setStatus(`检测失败: ${error.message}`, "error");
} finally {
els.checkExecBtn.disabled = false;
}
}
-
async function onRepairSessionHealth() {
const id = state.detail?.session?.id;
if (!id) {
setStatus("请先选择会话", "error");
return;
}
-
let health = state.sessionHealth;
if (!health || health.id !== id) {
try {
@@ -832,34 +659,21 @@
return;
}
}
-
if (!health.canRepair) {
setStatus("当前会话未检测到可修复的卡住状态", "error");
return;
}
-
const ok = await confirmDanger(`确认修复该会话的执行状态吗?turn_id=${health.repairTurnId || "-"}`, "确认修复");
- if (!ok) {
- return;
- }
-
+ if (!ok) return;
els.repairExecBtn.disabled = true;
try {
setStatus("正在修复会话执行状态...");
const data = await rpc("repairSessionHealth", { id, maxIdleSeconds: 600, reason: "interrupted" });
- if (state.selectedId !== id) {
- return;
- }
-
+ if (state.selectedId !== id) return;
state.sessionHealth = { id, ...(data.after || {}) };
renderSessionHealth();
await loadDetail(id, { silent: true });
-
- if (data.repaired) {
- setStatus(`修复完成,已备份: ${data.backupPath}`, "success");
- } else {
- setStatus("修复执行完成,但状态仍需人工确认", "error");
- }
+ setStatus(data.repaired ? `修复完成,已备份: ${data.backupPath}` : "修复执行完成,但状态仍需人工确认", data.repaired ? "success" : "error");
} catch (error) {
setStatus(`修复失败: ${error.message}`, "error");
} finally {
@@ -872,7 +686,6 @@
setStatus("请先选择会话", "error");
return;
}
-
try {
await rpc("copyResume", { id });
setStatus("Resume 命令已复制", "success");
@@ -880,14 +693,12 @@
setStatus(`复制失败: ${error.message}`, "error");
}
}
-
async function onCopySessionId() {
const id = state.detail?.session?.id || state.selectedId;
if (!id) {
setStatus("请先选择会话", "error");
return;
}
-
try {
await rpc("copySessionId", { id });
setStatus("会话 ID 已复制", "success");
@@ -895,14 +706,12 @@
setStatus(`复制会话 ID 失败: ${error.message}`, "error");
}
}
-
async function onRunResume() {
const session = state.detail?.session;
- if (!session || !session.id) {
+ if (!session?.id) {
setStatus("请先选择会话", "error");
return;
}
-
try {
await rpc("runResume", { id: session.id, cwd: session.cwd || "" });
setStatus("已在终端执行 Resume", "success");
@@ -910,27 +719,17 @@
setStatus(`执行失败: ${error.message}`, "error");
}
}
-
function onSearchInput() {
state.search = els.searchInput.value.trim();
- if (state.searchTimer) {
- clearTimeout(state.searchTimer);
- }
-
+ if (state.searchTimer) clearTimeout(state.searchTimer);
state.searchTimer = setTimeout(() => {
- loadList({ keepSelection: false }).catch((error) => {
- setStatus(`搜索失败: ${error.message}`, "error");
- });
+ loadList({ keepSelection: false }).catch((error) => setStatus(`搜索失败: ${error.message}`, "error"));
}, 220);
}
-
function bindEvents() {
els.globalRefreshBtn.addEventListener("click", onRefreshAll);
-
els.tabActiveBtn.addEventListener("click", async () => {
- if (state.mode === "active") {
- return;
- }
+ if (state.mode === "active") return;
state.mode = "active";
updateTabs();
state.selectedId = null;
@@ -942,11 +741,8 @@
setStatus(`切换失败: ${error.message}`, "error");
}
});
-
els.tabRecycleBtn.addEventListener("click", async () => {
- if (state.mode === "archive") {
- return;
- }
+ if (state.mode === "archive") return;
state.mode = "archive";
updateTabs();
state.selectedId = null;
@@ -958,8 +754,13 @@
setStatus(`切换失败: ${error.message}`, "error");
}
});
-
els.searchInput.addEventListener("input", onSearchInput);
+ els.searchInput.addEventListener("keydown", (event) => {
+ if (event.key !== "Enter") return;
+ if (state.searchTimer) clearTimeout(state.searchTimer);
+ state.search = els.searchInput.value.trim();
+ loadList({ keepSelection: false }).catch((error) => setStatus(`搜索失败: ${error.message}`, "error"));
+ });
els.mismatchOnlyBtn.addEventListener("click", async () => {
state.mismatchOnly = !state.mismatchOnly;
updateMismatchToggle();
@@ -969,35 +770,14 @@
setStatus(`筛选失败: ${error.message}`, "error");
}
});
- els.searchInput.addEventListener("keydown", (event) => {
- if (event.key !== "Enter") {
- return;
- }
-
- if (state.searchTimer) {
- clearTimeout(state.searchTimer);
- }
- state.search = els.searchInput.value.trim();
- loadList({ keepSelection: false }).catch((error) => {
- setStatus(`搜索失败: ${error.message}`, "error");
- });
- });
-
els.batchUpdateBtn.addEventListener("click", onBatchUpdateProvider);
els.batchProviderInput.addEventListener("keydown", (event) => {
- if (event.key === "Enter") {
- onBatchUpdateProvider();
- }
+ if (event.key === "Enter") onBatchUpdateProvider();
});
-
els.sessionList.addEventListener("click", (event) => {
const item = event.target.closest(".session-item[data-id]");
- if (!item) {
- return;
- }
- onSelectSession(item.dataset.id || "");
+ if (item) onSelectSession(item.dataset.id || "");
});
-
els.refreshDetailBtn.addEventListener("click", async () => {
if (!state.selectedId) {
setStatus("请先选择会话", "error");
@@ -1009,33 +789,26 @@
setStatus(`刷新详情失败: ${error.message}`, "error");
}
});
-
els.editProviderBtn.addEventListener("click", () => setProviderEditing(true));
els.cancelProviderBtn.addEventListener("click", () => setProviderEditing(false));
els.saveProviderBtn.addEventListener("click", onSaveProvider);
els.providerEditInput.addEventListener("keydown", (event) => {
- if (event.key === "Enter") {
- onSaveProvider();
- }
- if (event.key === "Escape") {
- setProviderEditing(false);
- }
+ if (event.key === "Enter") onSaveProvider();
+ if (event.key === "Escape") setProviderEditing(false);
});
els.repairProviderBtn.addEventListener("click", onRepairProvider);
els.checkExecBtn.addEventListener("click", onCheckSessionHealth);
els.repairExecBtn.addEventListener("click", onRepairSessionHealth);
-
els.deleteRestoreBtn.addEventListener("click", onDeleteOrRestore);
els.copyResumeBtn.addEventListener("click", onCopyResume);
els.copySessionIdBtn.addEventListener("click", onCopySessionId);
els.runResumeBtn.addEventListener("click", onRunResume);
}
-
async function bootstrap() {
+ initLayout();
updateTabs();
updateMismatchToggle();
bindEvents();
-
try {
await loadHealth();
await loadConfigProviders();
@@ -1044,26 +817,5 @@
setStatus(`初始化失败: ${error.message}`, "error");
}
}
-
bootstrap();
})();
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/package.json b/package.json
index 4311552..3bd7a70 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,10 @@
],
"activationEvents": [
"onCommand:codexSessionManager.open",
- "onView:codexSessionManager.main"
+ "onView:codexSessionManager.controls",
+ "onView:codexSessionManager.sessions",
+ "onView:codexSessionManager.details",
+ "onView:codexSessionManager.messages"
],
"main": "./extension.js",
"contributes": {
@@ -21,6 +24,10 @@
{
"command": "codexSessionManager.open",
"title": "Codex Session Manager: Open"
+ },
+ {
+ "command": "codexSessionManager.refresh",
+ "title": "Codex Session Manager: Refresh"
}
],
"configuration": {
@@ -45,16 +52,30 @@
"views": {
"codexSessionManager": [
{
- "id": "codexSessionManager.main",
- "name": "Session Manager",
+ "id": "codexSessionManager.controls",
+ "name": "Controls",
+ "type": "webview"
+ },
+ {
+ "id": "codexSessionManager.sessions",
+ "name": "Sessions"
+ },
+ {
+ "id": "codexSessionManager.details",
+ "name": "Details",
+ "type": "webview"
+ },
+ {
+ "id": "codexSessionManager.messages",
+ "name": "Messages",
"type": "webview"
}
]
},
"viewsWelcome": [
{
- "view": "codexSessionManager.main",
- "contents": "Open the session manager view.\\n[Open Manager](command:codexSessionManager.open)"
+ "view": "codexSessionManager.sessions",
+ "contents": "Open the session manager views.\\n[Refresh Sessions](command:codexSessionManager.refresh)"
}
]
},