Files

192 lines
6.0 KiB
JavaScript

//
// 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 };