Move to a terminal driven integration to have better interactions
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
//
|
||||
// launch.js — building the command that starts or resumes a session.
|
||||
//
|
||||
// Nova exposes no way for an extension to open its built-in terminal or type
|
||||
// into one (no terminal object in the API; `newTerminal:` in the app is an
|
||||
// Objective-C menu selector with no JS bridge). So the extension prepares the
|
||||
// command and puts it on the clipboard; you open Nova's terminal and paste.
|
||||
//
|
||||
// The upside of launching a plain CLI session is that it is a completely
|
||||
// ordinary one: stored under ~/.claude/projects, resumable from any terminal,
|
||||
// and eligible for Remote Control.
|
||||
//
|
||||
|
||||
const { conf, log, warn } = require("./util.js");
|
||||
const cli = require("./cli.js");
|
||||
|
||||
/** Quote a value for a POSIX shell. */
|
||||
function shellQuote(value) {
|
||||
const text = String(value);
|
||||
if (/^[A-Za-z0-9_./:@%+=-]+$/.test(text)) return text;
|
||||
return "'" + text.replace(/'/g, `'\\''`) + "'";
|
||||
}
|
||||
|
||||
/**
|
||||
* Flags shared by new and resumed sessions, taken from settings so a launched
|
||||
* session matches what the extension is configured for.
|
||||
*/
|
||||
function configuredFlags() {
|
||||
const flags = [];
|
||||
|
||||
const model = conf("claudenova.model", "");
|
||||
if (model) flags.push("--model", model);
|
||||
|
||||
const effort = conf("claudenova.effort", "");
|
||||
if (effort) flags.push("--effort", effort);
|
||||
|
||||
const mode = conf("claudenova.permissionMode", "");
|
||||
if (mode) flags.push("--permission-mode", mode);
|
||||
|
||||
const extra = String(conf("claudenova.extraArgs", "")).trim();
|
||||
if (extra) {
|
||||
for (const piece of extra.split(/\s+/)) flags.push(piece);
|
||||
}
|
||||
|
||||
return flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a launch command line.
|
||||
*
|
||||
* @param options.resume session id to resume
|
||||
* @param options.remoteControl enable Remote Control for the session
|
||||
* @param options.cd prefix with a `cd` into the workspace
|
||||
* @param options.binary executable path (defaults to the resolved one)
|
||||
*/
|
||||
function buildCommand(options = {}) {
|
||||
const parts = [];
|
||||
|
||||
if (options.cd !== false && nova.workspace.path) {
|
||||
parts.push("cd", shellQuote(nova.workspace.path), "&&");
|
||||
}
|
||||
|
||||
// `claude` on PATH reads better than an absolute path; fall back to the
|
||||
// resolved binary when the shell would not find it.
|
||||
parts.push(options.binary ? shellQuote(options.binary) : "claude");
|
||||
|
||||
if (options.resume) {
|
||||
parts.push("--resume", shellQuote(options.resume));
|
||||
}
|
||||
|
||||
if (options.remoteControl) {
|
||||
parts.push("--remote-control");
|
||||
}
|
||||
|
||||
for (const flag of configuredFlags()) {
|
||||
parts.push(shellQuote(flag));
|
||||
}
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve which executable spelling to use: bare `claude` when the login shell
|
||||
* can find it, otherwise the absolute path we resolved.
|
||||
*/
|
||||
async function preferredBinary() {
|
||||
try {
|
||||
const resolved = await cli.resolveBinary();
|
||||
const home = nova.environment["HOME"] || "";
|
||||
|
||||
// The standard install is on PATH for interactive shells; anything
|
||||
// unusual (a configured path, a version manager shim) is safer spelled
|
||||
// out in full.
|
||||
if (resolved === nova.path.join(home, ".local/bin/claude")) return null;
|
||||
if (resolved === "/opt/homebrew/bin/claude") return null;
|
||||
if (resolved === "/usr/local/bin/claude") return null;
|
||||
return resolved;
|
||||
} catch (err) {
|
||||
log("falling back to bare `claude`:", err.message || err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a launch command on the clipboard and tell the user it is ready.
|
||||
* Resolves with the command.
|
||||
*/
|
||||
async function copyCommand(options = {}) {
|
||||
const binary = await preferredBinary();
|
||||
const command = buildCommand(Object.assign({ binary: binary }, options));
|
||||
|
||||
try {
|
||||
await nova.clipboard.writeText(command);
|
||||
} catch (err) {
|
||||
warn("could not write to the clipboard:", err);
|
||||
nova.workspace.showErrorMessage(
|
||||
"Could not copy the command. Run this in a terminal:\n\n" + command
|
||||
);
|
||||
return command;
|
||||
}
|
||||
|
||||
const request = new NotificationRequest("claudenova-launch");
|
||||
request.title = options.resume ? "Resume command copied" : "Launch command copied";
|
||||
request.body =
|
||||
command + "\n\nOpen a terminal and paste it.";
|
||||
request.actions = ["OK", "Show Command"];
|
||||
|
||||
try {
|
||||
const response = await nova.notifications.add(request);
|
||||
if (response && response.actionIdx === 1) {
|
||||
nova.workspace.showInformativeMessage(command);
|
||||
}
|
||||
} catch (err) {
|
||||
// Notifications are a courtesy; the clipboard already has the command.
|
||||
log("could not post the launch notification:", err);
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildCommand,
|
||||
copyCommand,
|
||||
configuredFlags,
|
||||
shellQuote,
|
||||
preferredBinary,
|
||||
};
|
||||
Reference in New Issue
Block a user