From c587bd6998d477d7d6ab800241610e98c2f6743a Mon Sep 17 00:00:00 2001 From: PairZhu <1115306638@qq.com> Date: Sat, 11 Apr 2026 00:03:17 +0800 Subject: [PATCH] refactor: switch session manager to native multi-view layout --- extension.js | 963 +++++++++++++++++++++++++++++++++++++--------- media/webview.css | 361 +++++++++++------ media/webview.js | 694 +++++++++++---------------------- package.json | 31 +- 4 files changed, 1276 insertions(+), 773 deletions(-) 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", + `
+
+
Mode
+
+ + +
+
+
+
Filters
+
+
+
${escapeHtml(summary)}
+
+
+
Batch
+
+
+
+
+
Status
+
+
Config${escapeHtml(configText)}
+
CodeX Home${escapeHtml(state.codexHome || "-")}
+
Database${escapeHtml(state.dbPath || "-")}
+
+
+
${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)}
+
+
ID${escapeHtml(session.id)}
+
Source${escapeHtml(session.source || "-")}
+
更新${escapeHtml(formatDisplayTime(session.updatedAt))}
+
创建${escapeHtml(formatDisplayTime(session.createdAt))}
+
CWD${escapeHtml(session.cwd || "-")}
+
+
+
+
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 - - - -
-
-
Codex Session Manager
-
- Config 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)" } ] },