// // sidebar.js — the four sidebar sections. // // Session state, pending approvals, tool activity, and changed files. Each is // a flat list; the interesting behaviour lives in the context commands that // main.js registers against the selections here. // const { throttle, relativize, basename, oneLine, modeLabel, formatCost, formatTokens, } = require("./util.js"); const { friendlyToolName } = require("./session.js"); const { actionsFor } = require("./permissions.js"); const RELOAD_INTERVAL = 200; const MAX_ACTIVITY_ROWS = 120; const STATUS_LABEL = { idle: "Ready", starting: "Starting…", thinking: "Thinking…", working: "Working…", waiting: "Waiting for you", stopped: "Stopped", error: "Error", }; function item(name, options = {}) { const treeItem = new TreeItem(name, TreeItemCollapsibleState.None); if (options.descriptiveText) treeItem.descriptiveText = options.descriptiveText; if (options.tooltip) treeItem.tooltip = options.tooltip; if (options.image) treeItem.image = options.image; if (options.path) treeItem.path = options.path; if (options.command) treeItem.command = options.command; if (options.contextValue) treeItem.contextValue = options.contextValue; if (options.identifier) treeItem.identifier = options.identifier; return treeItem; } class SessionProvider { constructor(context) { this.context = context; } getChildren(element) { if (element) return []; const session = this.context.session; const auth = this.context.authState; const rows = []; rows.push({ kind: "status", name: STATUS_LABEL[session.status] || session.status, detail: session.statusDetail, }); rows.push({ kind: "account", auth: auth }); rows.push({ kind: "model", name: session.model || this.context.configuredModel() || "Default model", }); rows.push({ kind: "mode", name: session.permissionMode ? modeLabel(session.permissionMode) : this.context.configuredPermissionMode(), }); const cost = formatCost(session.usage.costUSD); const input = formatTokens(session.usage.inputTokens); const output = formatTokens(session.usage.outputTokens); if (cost || input || output) { rows.push({ kind: "usage", cost: cost, tokens: input || output ? `${input || "0"} in / ${output || "0"} out` : null, }); } return rows; } getTreeItem(element) { switch (element.kind) { case "status": { const treeItem = item(element.name, { descriptiveText: element.detail ? oneLine(element.detail, 60) : "", image: "__builtin.action", contextValue: "status", }); return treeItem; } case "account": { const auth = element.auth; const signedIn = auth && auth.loggedIn; return item(signedIn ? "Account" : "Not signed in", { descriptiveText: this.context.describeAuth(), command: signedIn ? "claudenova.authStatus" : "claudenova.signIn", contextValue: "account", tooltip: signedIn ? "Signed in through Claude Code" : "Sign in to Claude", }); } case "model": return item("Model", { descriptiveText: element.name, command: "claudenova.setModel", contextValue: "model", }); case "mode": return item("Permissions", { descriptiveText: element.name, command: "claudenova.setPermissionMode", contextValue: "mode", }); case "usage": return item("Usage", { descriptiveText: [element.cost, element.tokens].filter(Boolean).join(" · "), contextValue: "usage", }); default: return item(String(element.kind)); } } getParent() { return null; } } class ApprovalsProvider { constructor(context) { this.context = context; } getChildren(element) { if (!element) return this.context.session.approvals.slice(); // The choices for a request are rows beneath it, so a decision never // depends on a panel being open. if (element.request) { return actionsFor(element).map((action) => Object.assign({ approval: element }, action) ); } return []; } getTreeItem(element) { if (element.approval) { return item(element.label, { identifier: element.approval.requestId + ":" + element.id, contextValue: "approval-action", command: "claudenova.approvalAction", tooltip: element.label, }); } const approval = element; const request = approval.request; const detail = approval.preview ? `+${approval.preview.additions} −${approval.preview.deletions}` : oneLine(approval.summary, 50); const treeItem = new TreeItem( friendlyToolName(request.tool_name), TreeItemCollapsibleState.Expanded ); treeItem.descriptiveText = detail; treeItem.tooltip = approval.summary || request.tool_name; treeItem.image = "__builtin.action"; treeItem.contextValue = "approval"; treeItem.identifier = approval.requestId; return treeItem; } getParent(element) { return element && element.approval ? element.approval : null; } } class ActivityProvider { constructor(context) { this.context = context; } getChildren(element) { if (element) return []; const tools = this.context.session.entries.filter((entry) => entry.kind === "tool"); return tools.slice(-MAX_ACTIVITY_ROWS).reverse(); } getTreeItem(entry) { const marks = { running: "…", done: "", error: "failed", denied: "denied" }; const detail = [oneLine(entry.summary, 48), marks[entry.state]].filter(Boolean).join(" · "); const treeItem = item(friendlyToolName(entry.name), { descriptiveText: detail, tooltip: entry.summary || entry.name, identifier: entry.id, contextValue: entry.filePath ? "activity-file" : "activity", }); if (entry.filePath) { treeItem.path = entry.filePath; treeItem.command = "claudenova.openActivityFile"; } return treeItem; } getParent() { return null; } } class ChangesProvider { constructor(context) { this.context = context; } getChildren(element) { if (element) return []; return Array.from(this.context.session.changedFiles.values()); } getTreeItem(change) { const counts = change.additions !== null || change.deletions !== null ? `+${change.additions || 0} −${change.deletions || 0}` : null; const detail = [counts, change.edits > 1 ? `${change.edits} edits` : null] .filter(Boolean) .join(" · "); const treeItem = item(basename(change.path), { descriptiveText: detail, tooltip: relativize(change.path), path: change.path, identifier: change.path, contextValue: "change", command: "claudenova.openChangedFile", }); return treeItem; } getParent() { return null; } } class Sidebar { /** * @param context an object exposing `session`, `authState`, `describeAuth()`, * `configuredModel()` and `configuredPermissionMode()`. */ constructor(context) { this.context = context; this.sessionView = new TreeView("claude.session", { dataProvider: new SessionProvider(context) }); this.approvalsView = new TreeView("claude.approvals", { dataProvider: new ApprovalsProvider(context) }); this.activityView = new TreeView("claude.activity", { dataProvider: new ActivityProvider(context) }); this.changesView = new TreeView("claude.changes", { dataProvider: new ChangesProvider(context) }); this.views = [this.sessionView, this.approvalsView, this.activityView, this.changesView]; this.reload = throttle(() => { for (const view of this.views) { view.reload().catch(() => { /* the view may not be visible yet */ }); } }, RELOAD_INTERVAL); } dispose() { this.reload.cancel(); for (const view of this.views) { try { view.dispose(); } catch (_) { /* already disposed */ } } } } module.exports = { Sidebar };