// // sidebar.js — the two sidebar sections. // // Chat lives in a terminal, so the sidebar's job is everything around it: // account state, and the project's sessions with the actions that launch them. // const { throttle, oneLine, modeLabel, conf } = require("./util.js"); const RELOAD_INTERVAL = 200; function item(name, options = {}) { const treeItem = new TreeItem( name, options.collapsibleState || 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; } /** Actions offered under a session row. Ids are handled by main.js. */ function sessionActions() { return [ { id: "resume", label: "Copy Resume Command" }, { id: "resume-remote", label: "Copy Resume + Remote Control" }, { id: "reveal", label: "Reveal Session File" }, ]; } class StatusProvider { constructor(context) { this.context = context; } getChildren(element) { if (element) return []; const rows = [{ kind: "account" }, { kind: "model" }, { kind: "mode" }]; if (!this.context.workspacePath) rows.push({ kind: "no-workspace" }); return rows; } getTreeItem(element) { switch (element.kind) { case "account": { const auth = this.context.authState; const signedIn = auth && auth.loggedIn; return item(signedIn ? "Account" : "Not signed in", { descriptiveText: this.context.describeAuth(), command: signedIn ? "claudenova.authStatus" : "claudenova.signIn", contextValue: "account", image: "__builtin.action", tooltip: signedIn ? "Signed in through Claude Code" : "Sign in to Claude", }); } case "model": return item("Model", { descriptiveText: this.context.configuredModel(), command: "claudenova.setModel", contextValue: "model", tooltip: "Model used by sessions this extension launches", }); case "mode": return item("Permissions", { descriptiveText: modeLabel(conf("claudenova.permissionMode", "")) || "Claude Code default", command: "claudenova.setPermissionMode", contextValue: "mode", tooltip: "Permission mode used by sessions this extension launches", }); case "no-workspace": return item("No project open", { descriptiveText: "open a folder", tooltip: "Sessions are stored per project directory", }); default: return item(String(element.kind)); } } getParent() { return null; } } class SessionsProvider { constructor(context) { this.context = context; } getChildren(element) { if (!element) return this.context.sessions.slice(); // A session row expands into the things you can do with it. if (element.id && element.path) { return sessionActions().map((action) => Object.assign({ session: element }, action) ); } return []; } getTreeItem(element) { if (element.session) { return item(element.label, { identifier: element.session.id + ":" + element.id, contextValue: "session-action", command: "claudenova.sessionAction", tooltip: element.label, }); } const session = element; return item(oneLine(session.label, 60), { descriptiveText: session.relative, tooltip: `${session.label}\n${session.id}\n${session.modifiedAt.toLocaleString()}`, identifier: session.id, contextValue: "session", collapsibleState: TreeItemCollapsibleState.Collapsed, command: "claudenova.copyResume", }); } getParent(element) { return element && element.session ? element.session : null; } } class Sidebar { /** * @param context exposes `sessions`, `authState`, `workspacePath`, * `describeAuth()` and `configuredModel()`. */ constructor(context) { this.context = context; this.statusView = new TreeView("claude.status", { dataProvider: new StatusProvider(context), }); this.sessionsView = new TreeView("claude.sessions", { dataProvider: new SessionsProvider(context), }); this.views = [this.statusView, this.sessionsView]; this.visibilityHandlers = []; this.sessionsView.onDidChangeVisibility(() => { if (!this.sessionsView.visible) return; for (const handler of this.visibilityHandlers) handler(); }); this.reload = throttle(() => { for (const view of this.views) { view.reload().catch(() => { /* the view may not be visible yet */ }); } }, RELOAD_INTERVAL); } /** Run `handler` whenever the Sessions section becomes visible. */ onDidBecomeVisible(handler) { this.visibilityHandlers.push(handler); } dispose() { this.reload.cancel(); for (const view of this.views) { try { view.dispose(); } catch (_) { /* already disposed */ } } } } module.exports = { Sidebar, sessionActions };