192 lines
6.0 KiB
JavaScript
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 };
|