First version
This commit is contained in:
+744
@@ -0,0 +1,744 @@
|
||||
//
|
||||
// main.js — extension entry point.
|
||||
//
|
||||
// Owns the lifecycle: one Claude Code process per window, a session model fed
|
||||
// from its output, and the views and commands that surface it.
|
||||
//
|
||||
|
||||
const { conf, log, warn, relativize, oneLine } = require("./util.js");
|
||||
const cli = require("./cli.js");
|
||||
const auth = require("./auth.js");
|
||||
const diff = require("./diff.js");
|
||||
const store = require("./sessions-store.js");
|
||||
const { ClaudeClient } = require("./client.js");
|
||||
const { Session } = require("./session.js");
|
||||
const { Transcript } = require("./transcript.js");
|
||||
const { Sidebar } = require("./sidebar.js");
|
||||
const { PermissionBroker } = require("./permissions.js");
|
||||
|
||||
const MODELS = [
|
||||
["", "Default (from your Claude Code settings)"],
|
||||
["opus", "Opus"],
|
||||
["sonnet", "Sonnet"],
|
||||
["haiku", "Haiku"],
|
||||
["fable", "Fable"],
|
||||
];
|
||||
|
||||
const PERMISSION_MODES = [
|
||||
["manual", "Manual — ask before each tool"],
|
||||
["acceptEdits", "Accept Edits — auto-approve file edits"],
|
||||
["plan", "Plan — research only, no changes"],
|
||||
["auto", "Auto — Claude decides what needs asking"],
|
||||
["bypassPermissions", "Bypass — approve everything (dangerous)"],
|
||||
];
|
||||
|
||||
class Controller {
|
||||
constructor() {
|
||||
this.session = new Session();
|
||||
this.transcript = new Transcript(this.session);
|
||||
this.broker = new PermissionBroker(this.session, this.transcript);
|
||||
this.client = null;
|
||||
this.authState = null;
|
||||
this.sidebar = null;
|
||||
this.starting = null;
|
||||
}
|
||||
|
||||
// -- lifecycle ---------------------------------------------------------
|
||||
|
||||
activate() {
|
||||
this.transcript.showThinking = conf("claudenova.showThinking", true) !== false;
|
||||
|
||||
const controller = this;
|
||||
this.sidebar = new Sidebar({
|
||||
session: this.session,
|
||||
get authState() {
|
||||
return controller.authState;
|
||||
},
|
||||
describeAuth: () => auth.describe(this.authState),
|
||||
configuredModel: () => this.labelFor(MODELS, conf("claudenova.model", "")),
|
||||
configuredPermissionMode: () =>
|
||||
this.labelFor(PERMISSION_MODES, conf("claudenova.permissionMode", "manual")),
|
||||
});
|
||||
|
||||
this.session.onChange.on(() => {
|
||||
this.sidebar.reload();
|
||||
this.transcript.update();
|
||||
});
|
||||
|
||||
this.registerCommands();
|
||||
this.watchConfig();
|
||||
|
||||
this.refreshAuth().catch((err) => log("initial auth check failed:", err));
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.client) this.client.stop(true);
|
||||
this.transcript.dispose();
|
||||
if (this.sidebar) this.sidebar.dispose();
|
||||
}
|
||||
|
||||
labelFor(table, value) {
|
||||
const row = table.find((entry) => entry[0] === (value || ""));
|
||||
return row ? row[1] : value;
|
||||
}
|
||||
|
||||
watchConfig() {
|
||||
const reloadThinking = () => {
|
||||
this.transcript.showThinking = conf("claudenova.showThinking", true) !== false;
|
||||
this.transcript.update();
|
||||
};
|
||||
|
||||
nova.subscriptions.add(nova.config.onDidChange("claudenova.showThinking", reloadThinking));
|
||||
nova.subscriptions.add(
|
||||
nova.workspace.config.onDidChange("claudenova.showThinking", reloadThinking)
|
||||
);
|
||||
nova.subscriptions.add(
|
||||
nova.config.onDidChange("claudenova.binaryPath", () => cli.forgetBinary())
|
||||
);
|
||||
}
|
||||
|
||||
// -- account -----------------------------------------------------------
|
||||
|
||||
async refreshAuth() {
|
||||
this.authState = await auth.status();
|
||||
this.sidebar.reload();
|
||||
return this.authState;
|
||||
}
|
||||
|
||||
// -- session -----------------------------------------------------------
|
||||
|
||||
sessionOptions(overrides = {}) {
|
||||
const extra = String(conf("claudenova.extraArgs", "")).trim();
|
||||
|
||||
return Object.assign(
|
||||
{
|
||||
cwd: nova.workspace.path || undefined,
|
||||
model: conf("claudenova.model", "") || undefined,
|
||||
effort: conf("claudenova.effort", "") || undefined,
|
||||
permissionMode: conf("claudenova.permissionMode", "manual"),
|
||||
streamPartial: conf("claudenova.streamPartial", true) !== false,
|
||||
extraArgs: extra ? extra.split(/\s+/) : [],
|
||||
},
|
||||
overrides
|
||||
);
|
||||
}
|
||||
|
||||
/** Start a session if one is not already running. */
|
||||
async ensureSession() {
|
||||
if (this.client && this.client.isRunning) return this.client;
|
||||
if (this.starting) return this.starting;
|
||||
|
||||
this.starting = this.startSession().finally(() => {
|
||||
this.starting = null;
|
||||
});
|
||||
return this.starting;
|
||||
}
|
||||
|
||||
async startSession(overrides = {}) {
|
||||
if (this.client) this.client.stop();
|
||||
|
||||
this.clearStagedProposals();
|
||||
this.session.reset();
|
||||
this.session.setStatus("starting");
|
||||
|
||||
const client = new ClaudeClient({
|
||||
onMessage: (message) => this.session.handleMessage(message),
|
||||
onPermission: (request, requestId) => this.broker.handle(request, requestId),
|
||||
onPermissionCancelled: (requestId) => this.broker.cancel(requestId),
|
||||
onExit: (status, stderr) => this.handleExit(status, stderr),
|
||||
});
|
||||
|
||||
this.client = client;
|
||||
|
||||
try {
|
||||
await client.start(this.sessionOptions(overrides));
|
||||
} catch (err) {
|
||||
this.client = null;
|
||||
this.session.setStatus("error", String(err.message || err));
|
||||
throw err;
|
||||
}
|
||||
|
||||
this.session.sessionId = client.sessionId;
|
||||
this.session.setStatus("idle");
|
||||
|
||||
if (conf("claudenova.openTranscript", true) !== false) {
|
||||
await this.transcript.show();
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/** Remove proposal files left over from a previous session. */
|
||||
clearStagedProposals() {
|
||||
const root = nova.path.join(
|
||||
nova.workspace.path
|
||||
? nova.extension.workspaceStoragePath
|
||||
: nova.extension.globalStoragePath,
|
||||
"proposed"
|
||||
);
|
||||
|
||||
try {
|
||||
if (!nova.fs.access(root, nova.fs.F_OK)) return;
|
||||
for (const name of nova.fs.listdir(root)) {
|
||||
nova.fs.remove(nova.path.join(root, name));
|
||||
}
|
||||
} catch (err) {
|
||||
log("could not clear staged proposals:", err);
|
||||
}
|
||||
}
|
||||
|
||||
handleExit(status, stderr) {
|
||||
const wasBusy = this.session.isBusy;
|
||||
this.session.setStatus("stopped");
|
||||
|
||||
for (const approval of this.session.approvals.slice()) {
|
||||
approval.settled = true;
|
||||
this.session.removeApproval(approval.requestId);
|
||||
}
|
||||
|
||||
if (status !== 0 && wasBusy) {
|
||||
const detail = oneLine(stderr, 300) || `Claude Code exited with status ${status}.`;
|
||||
this.session.addNotice(detail, "error");
|
||||
nova.workspace.showErrorMessage("Claude Code stopped unexpectedly.\n\n" + detail);
|
||||
}
|
||||
|
||||
this.transcript.flush().catch(() => {
|
||||
/* the tab may be closed */
|
||||
});
|
||||
}
|
||||
|
||||
// -- asking ------------------------------------------------------------
|
||||
|
||||
async ask(text, context) {
|
||||
if (!text || !text.trim()) return;
|
||||
|
||||
try {
|
||||
await this.ensureSession();
|
||||
} catch (err) {
|
||||
nova.workspace.showErrorMessage(String(err.message || err));
|
||||
return;
|
||||
}
|
||||
|
||||
this.session.addUserTurn(text, context ? context.label : null);
|
||||
await this.transcript.show();
|
||||
|
||||
try {
|
||||
this.client.send(context ? context.content(text) : text);
|
||||
} catch (err) {
|
||||
this.session.setStatus("error", String(err.message || err));
|
||||
nova.workspace.showErrorMessage("Could not send your message: " + (err.message || err));
|
||||
}
|
||||
}
|
||||
|
||||
promptAndAsk(prefill, context) {
|
||||
nova.workspace.showInputPalette(
|
||||
context ? `Ask Claude about ${context.label}` : "Ask Claude",
|
||||
{ placeholder: "What would you like Claude to do?", value: prefill || "" },
|
||||
(value) => {
|
||||
if (value === null) return;
|
||||
this.ask(value, context);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Build the context payload for the current editor selection. */
|
||||
selectionContext(editor) {
|
||||
if (!editor) return null;
|
||||
if (conf("claudenova.includeSelection", true) === false) return null;
|
||||
|
||||
const range = editor.selectedRange;
|
||||
if (!range || range.empty) return null;
|
||||
|
||||
const document = editor.document;
|
||||
const path = document.path;
|
||||
if (!path) return null;
|
||||
|
||||
const before = document.getTextInRange(new Range(0, range.start));
|
||||
const startLine = before.split("\n").length;
|
||||
const selected = document.getTextInRange(range);
|
||||
const endLine = startLine + selected.split("\n").length - 1;
|
||||
|
||||
const label = `${relativize(path)}:${startLine}-${endLine}`;
|
||||
const syntax = document.syntax || "";
|
||||
|
||||
return {
|
||||
label: label,
|
||||
content: (question) =>
|
||||
[
|
||||
question,
|
||||
"",
|
||||
`From \`${relativize(path)}\` lines ${startLine}–${endLine}:`,
|
||||
"```" + syntax,
|
||||
selected,
|
||||
"```",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
fileContext(editor) {
|
||||
const path = editor && editor.document && editor.document.path;
|
||||
if (!path) return null;
|
||||
|
||||
return {
|
||||
label: relativize(path),
|
||||
content: (question) => `${question}\n\nThe file in question is \`${path}\`.`,
|
||||
};
|
||||
}
|
||||
|
||||
// -- commands ----------------------------------------------------------
|
||||
|
||||
registerCommands() {
|
||||
const register = (name, callback) => {
|
||||
nova.subscriptions.add(nova.commands.register(name, callback));
|
||||
};
|
||||
|
||||
register("claudenova.ask", (editor) => {
|
||||
const context = TextEditor.isTextEditor(editor)
|
||||
? this.selectionContext(editor)
|
||||
: this.selectionContext(nova.workspace.activeTextEditor);
|
||||
this.promptAndAsk("", context);
|
||||
});
|
||||
|
||||
register("claudenova.askAboutSelection", (editor) => {
|
||||
const context = this.selectionContext(editor);
|
||||
if (!context) {
|
||||
nova.workspace.showWarningMessage("Select some code first, then ask Claude about it.");
|
||||
return;
|
||||
}
|
||||
this.promptAndAsk("", context);
|
||||
});
|
||||
|
||||
register("claudenova.addFiles", () => this.commandAddFiles());
|
||||
|
||||
register("claudenova.addFileContext", (editor) => {
|
||||
const context = this.fileContext(editor);
|
||||
if (!context) {
|
||||
nova.workspace.showWarningMessage("Save the file first so Claude can read it.");
|
||||
return;
|
||||
}
|
||||
this.promptAndAsk("", context);
|
||||
});
|
||||
|
||||
register("claudenova.plan", (editor) => {
|
||||
const context = TextEditor.isTextEditor(editor)
|
||||
? this.selectionContext(editor)
|
||||
: this.selectionContext(nova.workspace.activeTextEditor);
|
||||
this.commandPlan(context);
|
||||
});
|
||||
|
||||
register("claudenova.showTranscript", () => {
|
||||
this.transcript.show().catch((err) => warn("could not open the transcript:", err));
|
||||
});
|
||||
|
||||
register("claudenova.newSession", () => this.commandNewSession());
|
||||
register("claudenova.resumeSession", () => this.commandResumeSession());
|
||||
register("claudenova.interrupt", () => this.commandInterrupt());
|
||||
register("claudenova.endSession", () => this.commandEndSession());
|
||||
|
||||
register("claudenova.setModel", () => this.commandSetModel());
|
||||
register("claudenova.setPermissionMode", () => this.commandSetPermissionMode());
|
||||
|
||||
register("claudenova.signIn", () => this.commandSignIn());
|
||||
register("claudenova.signOut", () => this.commandSignOut());
|
||||
register("claudenova.authStatus", () => this.commandAuthStatus());
|
||||
register("claudenova.refreshStatus", () => this.refreshAuth());
|
||||
register("claudenova.setApiKey", () => this.commandSetApiKey());
|
||||
register("claudenova.clearApiKey", () => this.commandClearApiKey());
|
||||
|
||||
register("claudenova.approvalAction", () => this.commandApprovalAction());
|
||||
register("claudenova.reviewApproval", () => this.approvalCommand("review"));
|
||||
register("claudenova.allowApproval", () => this.approvalCommand("allow"));
|
||||
register("claudenova.allowApprovalAlways", () => this.approvalCommand("always"));
|
||||
register("claudenova.denyApproval", () => this.approvalCommand("deny"));
|
||||
|
||||
register("claudenova.openActivityFile", () => this.commandOpenActivityFile());
|
||||
register("claudenova.copyActivity", () => this.commandCopyActivity());
|
||||
|
||||
register("claudenova.openChangedFile", () => this.commandOpenChangedFile());
|
||||
register("claudenova.showChangeDiff", () => this.commandShowChangeDiff());
|
||||
register("claudenova.clearChanges", () => this.session.clearChangedFiles());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask in plan mode: Claude researches and proposes, changing nothing until
|
||||
* you approve the plan. Switches a running session over rather than
|
||||
* starting a new one, so the existing context carries into the planning.
|
||||
*/
|
||||
async commandPlan(context) {
|
||||
try {
|
||||
await this.ensureSession();
|
||||
} catch (err) {
|
||||
nova.workspace.showErrorMessage(String(err.message || err));
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.session.permissionMode !== "plan") {
|
||||
try {
|
||||
await this.client.setPermissionMode("plan");
|
||||
this.session.permissionMode = "plan";
|
||||
this.session.changed();
|
||||
} catch (err) {
|
||||
warn("could not switch to plan mode:", err);
|
||||
nova.workspace.showWarningMessage(
|
||||
"Could not switch to plan mode: " + (err.message || err)
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
nova.workspace.showInputPalette(
|
||||
context ? `Plan with Claude about ${context.label}` : "Plan with Claude",
|
||||
{ placeholder: "What should Claude plan?" },
|
||||
(value) => {
|
||||
if (value === null) return;
|
||||
this.ask(value, context);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick files (or a directory) to put in front of Claude, with the current
|
||||
* selection's line range attached when there is one. The editor-menu
|
||||
* command covers the active file; this covers everything else.
|
||||
*/
|
||||
commandAddFiles() {
|
||||
nova.workspace.showFileChooser(
|
||||
"Choose files for Claude to work with",
|
||||
{
|
||||
prompt: "Add",
|
||||
allowFiles: true,
|
||||
allowDirectories: true,
|
||||
allowMultiple: true,
|
||||
},
|
||||
(paths) => {
|
||||
if (!paths || paths.length === 0) return;
|
||||
|
||||
const listed = paths.map((path) => "`" + relativize(path) + "`").join(", ");
|
||||
const label = paths.length === 1 ? relativize(paths[0]) : `${paths.length} files`;
|
||||
|
||||
this.promptAndAsk("", {
|
||||
label: label,
|
||||
content: (question) =>
|
||||
`${question}\n\nWork with ${paths.length === 1 ? "this file" : "these files"}: ${listed}`,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async commandNewSession() {
|
||||
try {
|
||||
await this.startSession();
|
||||
this.session.addNotice("New session started.", "info");
|
||||
} catch (err) {
|
||||
nova.workspace.showErrorMessage(String(err.message || err));
|
||||
}
|
||||
}
|
||||
|
||||
commandResumeSession() {
|
||||
const sessions = store.recentSessions(nova.workspace.path);
|
||||
if (sessions.length === 0) {
|
||||
nova.workspace.showInformativeMessage("No previous Claude sessions were found for this project.");
|
||||
return;
|
||||
}
|
||||
|
||||
const choices = sessions.map((session) => {
|
||||
const when = session.modifiedAt.toLocaleString();
|
||||
return `${session.label} — ${when}`;
|
||||
});
|
||||
|
||||
nova.workspace.showChoicePalette(
|
||||
choices,
|
||||
{ placeholder: "Resume a Claude session" },
|
||||
(choice, index) => {
|
||||
if (index === null || index === undefined) return;
|
||||
const picked = sessions[index];
|
||||
this.startSession({ resume: picked.id })
|
||||
.then(() => {
|
||||
this.session.addNotice(`Resumed session ${picked.id.slice(0, 8)}.`, "info");
|
||||
})
|
||||
.catch((err) => nova.workspace.showErrorMessage(String(err.message || err)));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async commandInterrupt() {
|
||||
if (!this.client || !this.client.isRunning) {
|
||||
nova.workspace.showInformativeMessage("No Claude session is running.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.client.interrupt();
|
||||
this.session.addNotice("Interrupted.", "info");
|
||||
this.session.setStatus("idle");
|
||||
} catch (err) {
|
||||
warn("interrupt failed:", err);
|
||||
nova.workspace.showWarningMessage("Could not interrupt Claude: " + (err.message || err));
|
||||
}
|
||||
}
|
||||
|
||||
commandEndSession() {
|
||||
if (!this.client) {
|
||||
nova.workspace.showInformativeMessage("No Claude session is running.");
|
||||
return;
|
||||
}
|
||||
this.client.stop();
|
||||
this.client = null;
|
||||
this.session.setStatus("stopped");
|
||||
this.transcript.flush().catch(() => {
|
||||
/* the tab may be closed */
|
||||
});
|
||||
}
|
||||
|
||||
commandSetModel() {
|
||||
const labels = MODELS.map((entry) => entry[1]);
|
||||
nova.workspace.showChoicePalette(labels, { placeholder: "Model" }, (choice, index) => {
|
||||
if (index === null || index === undefined) return;
|
||||
|
||||
const value = MODELS[index][0];
|
||||
nova.workspace.config.set("claudenova.model", value);
|
||||
|
||||
if (this.client && this.client.isRunning) {
|
||||
this.client
|
||||
.setModel(value || undefined)
|
||||
.then(() => {
|
||||
this.session.model = value || null;
|
||||
this.session.changed();
|
||||
})
|
||||
.catch((err) => warn("could not change the model:", err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
commandSetPermissionMode() {
|
||||
const labels = PERMISSION_MODES.map((entry) => entry[1]);
|
||||
nova.workspace.showChoicePalette(labels, { placeholder: "Permission mode" }, (choice, index) => {
|
||||
if (index === null || index === undefined) return;
|
||||
|
||||
const value = PERMISSION_MODES[index][0];
|
||||
nova.workspace.config.set("claudenova.permissionMode", value);
|
||||
|
||||
if (this.client && this.client.isRunning) {
|
||||
this.client
|
||||
.setPermissionMode(value)
|
||||
.then(() => {
|
||||
this.session.permissionMode = value;
|
||||
this.session.changed();
|
||||
})
|
||||
.catch((err) => warn("could not change the permission mode:", err));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// -- account commands --------------------------------------------------
|
||||
|
||||
commandSignIn() {
|
||||
nova.workspace.showActionPanel(
|
||||
"How would you like to sign in to Claude?",
|
||||
{
|
||||
buttons: ["Claude Subscription", "Anthropic Console", "Open a Terminal", "Cancel"],
|
||||
},
|
||||
(index) => {
|
||||
if (index === null || index === 3) return;
|
||||
|
||||
if (index === 2) {
|
||||
auth.signInViaTerminal()
|
||||
.then(() => {
|
||||
nova.workspace.showInformativeMessage(
|
||||
"Finish signing in in the terminal window, then use Refresh in the Claude sidebar."
|
||||
);
|
||||
})
|
||||
.catch((err) => nova.workspace.showErrorMessage(String(err.message || err)));
|
||||
return;
|
||||
}
|
||||
|
||||
auth.signIn({ console: index === 1 })
|
||||
.then(async (ok) => {
|
||||
if (!ok) return;
|
||||
await this.refreshAuth();
|
||||
nova.workspace.showInformativeMessage("Signed in to Claude as " + auth.describe(this.authState));
|
||||
})
|
||||
.catch((err) => {
|
||||
nova.workspace.showErrorMessage(
|
||||
String(err.message || err) +
|
||||
"\n\nYou can also sign in by running `claude auth login` in a terminal."
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
commandSignOut() {
|
||||
auth.signOut()
|
||||
.then(() => this.refreshAuth())
|
||||
.then(() => nova.workspace.showInformativeMessage("Signed out of Claude."))
|
||||
.catch((err) => nova.workspace.showErrorMessage(String(err.message || err)));
|
||||
}
|
||||
|
||||
async commandAuthStatus() {
|
||||
const state = await this.refreshAuth();
|
||||
const lines = [];
|
||||
|
||||
if (!state.available) {
|
||||
lines.push("Claude Code was not found on this machine.");
|
||||
if (state.error) lines.push(state.error);
|
||||
} else if (state.loggedIn) {
|
||||
lines.push("Signed in to Claude.");
|
||||
if (state.email) lines.push("Account: " + state.email);
|
||||
if (state.subscriptionType) lines.push("Plan: " + state.subscriptionType);
|
||||
if (state.orgName) lines.push("Organization: " + state.orgName);
|
||||
if (state.authMethod) lines.push("Method: " + state.authMethod);
|
||||
} else {
|
||||
lines.push("Not signed in.");
|
||||
if (state.error) lines.push(state.error);
|
||||
}
|
||||
|
||||
if (cli.storedApiKey()) {
|
||||
lines.push("");
|
||||
lines.push("An API key stored in your keychain is being passed to Claude Code.");
|
||||
}
|
||||
|
||||
const version = await cli.version().catch(() => null);
|
||||
if (version) {
|
||||
lines.push("");
|
||||
lines.push("Claude Code " + version);
|
||||
}
|
||||
|
||||
nova.workspace.showInformativeMessage(lines.join("\n"));
|
||||
}
|
||||
|
||||
commandSetApiKey() {
|
||||
nova.workspace.showInputPanel(
|
||||
"Store an Anthropic API key in your keychain. Claude Code will use it instead of your signed-in account.",
|
||||
{ label: "API key", placeholder: "sk-ant-…", secure: true, prompt: "Save" },
|
||||
(value) => {
|
||||
if (value === null) return;
|
||||
const key = value.trim();
|
||||
if (!key) return;
|
||||
|
||||
try {
|
||||
cli.setStoredApiKey(key);
|
||||
nova.workspace.showInformativeMessage(
|
||||
"Saved. Start a new Claude session for it to take effect."
|
||||
);
|
||||
this.refreshAuth();
|
||||
} catch (err) {
|
||||
nova.workspace.showErrorMessage("Could not save the key: " + (err.message || err));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
commandClearApiKey() {
|
||||
cli.clearStoredApiKey();
|
||||
nova.workspace.showInformativeMessage(
|
||||
"Removed the stored API key. Start a new Claude session for it to take effect."
|
||||
);
|
||||
this.refreshAuth();
|
||||
}
|
||||
|
||||
// -- approval commands -------------------------------------------------
|
||||
|
||||
/** An action row under a pending request was activated. */
|
||||
commandApprovalAction() {
|
||||
const selection = this.sidebar.approvalsView.selection;
|
||||
const row = selection && selection[0];
|
||||
if (!row || !row.approval) return;
|
||||
this.broker.decide(row.approval, row.id);
|
||||
}
|
||||
|
||||
approvalCommand(choice) {
|
||||
const selection = this.sidebar.approvalsView.selection;
|
||||
const selected = selection && selection[0];
|
||||
|
||||
// Selecting an action row and using the context menu should still act
|
||||
// on its parent request.
|
||||
const approval =
|
||||
(selected && (selected.approval || (selected.request ? selected : null))) ||
|
||||
this.session.approvals[0];
|
||||
|
||||
if (!approval) {
|
||||
nova.workspace.showInformativeMessage("Nothing is waiting for approval.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.broker.decide(approval, choice);
|
||||
}
|
||||
|
||||
// -- activity and changes ----------------------------------------------
|
||||
|
||||
commandOpenActivityFile() {
|
||||
const selection = this.sidebar.activityView.selection;
|
||||
const entry = selection && selection[0];
|
||||
if (!entry) return;
|
||||
|
||||
if (entry.filePath) {
|
||||
nova.workspace.openFile(entry.filePath);
|
||||
} else {
|
||||
this.commandCopyActivity();
|
||||
}
|
||||
}
|
||||
|
||||
commandCopyActivity() {
|
||||
const selection = this.sidebar.activityView.selection;
|
||||
const entry = selection && selection[0];
|
||||
if (!entry) return;
|
||||
|
||||
const details = [
|
||||
`${entry.name} — ${entry.state}`,
|
||||
"",
|
||||
JSON.stringify(entry.input, null, 2),
|
||||
];
|
||||
if (entry.result) {
|
||||
details.push("", "Result:", entry.result);
|
||||
}
|
||||
|
||||
nova.clipboard.writeText(details.join("\n"));
|
||||
nova.workspace.showInformativeMessage("Copied the tool call to the clipboard.");
|
||||
}
|
||||
|
||||
commandOpenChangedFile() {
|
||||
const selection = this.sidebar.changesView.selection;
|
||||
for (const change of selection || []) {
|
||||
nova.workspace.openFile(change.path);
|
||||
}
|
||||
}
|
||||
|
||||
commandShowChangeDiff() {
|
||||
const selection = this.sidebar.changesView.selection;
|
||||
const change = selection && selection[0];
|
||||
if (!change) return;
|
||||
|
||||
const tool = this.session.entries
|
||||
.filter((entry) => entry.kind === "tool" && entry.filePath === change.path)
|
||||
.pop();
|
||||
|
||||
if (!tool) {
|
||||
nova.workspace.openFile(change.path);
|
||||
return;
|
||||
}
|
||||
|
||||
const preview = diff.previewForTool(tool.name, tool.input);
|
||||
if (preview) {
|
||||
diff.showPreview(preview);
|
||||
} else {
|
||||
nova.workspace.openFile(change.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const controller = new Controller();
|
||||
|
||||
exports.activate = function () {
|
||||
log("activating");
|
||||
try {
|
||||
controller.activate();
|
||||
} catch (err) {
|
||||
warn("activation failed:", err);
|
||||
nova.workspace.showErrorMessage("The Claude extension failed to start: " + (err.message || err));
|
||||
}
|
||||
};
|
||||
|
||||
exports.deactivate = function () {
|
||||
controller.dispose();
|
||||
};
|
||||
Reference in New Issue
Block a user