Move to a terminal driven integration to have better interactions
This commit is contained in:
+75
-189
@@ -1,38 +1,19 @@
|
||||
//
|
||||
// sidebar.js — the four sidebar sections.
|
||||
// sidebar.js — the two 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.
|
||||
// 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,
|
||||
relativize,
|
||||
basename,
|
||||
oneLine,
|
||||
modeLabel,
|
||||
formatCost,
|
||||
formatTokens,
|
||||
} = require("./util.js");
|
||||
const { friendlyToolName } = require("./session.js");
|
||||
const { actionsFor } = require("./permissions.js");
|
||||
const { throttle, oneLine, modeLabel, conf } = require("./util.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);
|
||||
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;
|
||||
@@ -43,7 +24,16 @@ function item(name, options = {}) {
|
||||
return treeItem;
|
||||
}
|
||||
|
||||
class SessionProvider {
|
||||
/** 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;
|
||||
}
|
||||
@@ -51,84 +41,48 @@ class SessionProvider {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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 "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 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",
|
||||
tooltip: signedIn ? "Signed in through Claude Code" : "Sign in to Claude",
|
||||
image: "__builtin.action",
|
||||
tooltip: signedIn
|
||||
? "Signed in through Claude Code"
|
||||
: "Sign in to Claude",
|
||||
});
|
||||
}
|
||||
|
||||
case "model":
|
||||
return item("Model", {
|
||||
descriptiveText: element.name,
|
||||
descriptiveText: this.context.configuredModel(),
|
||||
command: "claudenova.setModel",
|
||||
contextValue: "model",
|
||||
tooltip: "Model used by sessions this extension launches",
|
||||
});
|
||||
|
||||
case "mode":
|
||||
return item("Permissions", {
|
||||
descriptiveText: element.name,
|
||||
descriptiveText:
|
||||
modeLabel(conf("claudenova.permissionMode", "")) || "Claude Code default",
|
||||
command: "claudenova.setPermissionMode",
|
||||
contextValue: "mode",
|
||||
tooltip: "Permission mode used by sessions this extension launches",
|
||||
});
|
||||
|
||||
case "usage":
|
||||
return item("Usage", {
|
||||
descriptiveText: [element.cost, element.tokens].filter(Boolean).join(" · "),
|
||||
contextValue: "usage",
|
||||
case "no-workspace":
|
||||
return item("No project open", {
|
||||
descriptiveText: "open a folder",
|
||||
tooltip: "Sessions are stored per project directory",
|
||||
});
|
||||
|
||||
default:
|
||||
@@ -141,19 +95,18 @@ class SessionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
class ApprovalsProvider {
|
||||
class SessionsProvider {
|
||||
constructor(context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
getChildren(element) {
|
||||
if (!element) return this.context.session.approvals.slice();
|
||||
if (!element) return this.context.sessions.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)
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,125 +114,53 @@ class ApprovalsProvider {
|
||||
}
|
||||
|
||||
getTreeItem(element) {
|
||||
if (element.approval) {
|
||||
if (element.session) {
|
||||
return item(element.label, {
|
||||
identifier: element.approval.requestId + ":" + element.id,
|
||||
contextValue: "approval-action",
|
||||
command: "claudenova.approvalAction",
|
||||
identifier: element.session.id + ":" + element.id,
|
||||
contextValue: "session-action",
|
||||
command: "claudenova.sessionAction",
|
||||
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;
|
||||
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.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;
|
||||
return element && element.session ? element.session : null;
|
||||
}
|
||||
}
|
||||
|
||||
class Sidebar {
|
||||
/**
|
||||
* @param context an object exposing `session`, `authState`, `describeAuth()`,
|
||||
* `configuredModel()` and `configuredPermissionMode()`.
|
||||
* @param context exposes `sessions`, `authState`, `workspacePath`,
|
||||
* `describeAuth()` and `configuredModel()`.
|
||||
*/
|
||||
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.statusView = new TreeView("claude.status", {
|
||||
dataProvider: new StatusProvider(context),
|
||||
});
|
||||
this.sessionsView = new TreeView("claude.sessions", {
|
||||
dataProvider: new SessionsProvider(context),
|
||||
});
|
||||
|
||||
this.views = [this.sessionView, this.approvalsView, this.activityView, this.changesView];
|
||||
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) {
|
||||
@@ -290,6 +171,11 @@ class Sidebar {
|
||||
}, 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) {
|
||||
@@ -302,4 +188,4 @@ class Sidebar {
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Sidebar };
|
||||
module.exports = { Sidebar, sessionActions };
|
||||
|
||||
Reference in New Issue
Block a user