First version
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user