First version

This commit is contained in:
2026-08-11 12:38:02 -03:00
parent 42e875d866
commit 89d1a06204
37 changed files with 5660 additions and 4 deletions
+305
View File
@@ -0,0 +1,305 @@
//
// 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 };