perf: faster plugin updates, half-size first bundle, images from the chat attach on Windows
- Chat view: an attached image's path is pasted on its own after the text (promptForPane). claude turns a paste that is only an image path into [Image #n], but left a Windows path inside a paste of text as a plain path. Other files and @-mentions stay in the text as before. - Plugin bootstrap: depsHash covers only what npm installs (no release version), so a version bump no longer reinstalls runtime deps; the client's installed tree is kept while its manifests are unchanged (no npm ci per update); the user-side build runs vite only (tsc -b cost ~25 s / ~900 MB); clientBuild is recorded only after a build that did not fail, so a failed one is retried; npm output goes to runtime/npm-install.log. - Server: a port held by another program ends with a clear message and exit 1 instead of an uncaught 'error' stack trace. - Client: the onboarding wizard is lazy and rendered only before onboarding; usePrefersReducedMotion moved to hooks/ so PetSettings no longer pulls the Sagi chat/pane stack into the entry chunk (884 KB -> 431 KB, gzip 268 -> 133 KB).
This commit is contained in:
+50
-12
@@ -138,14 +138,38 @@ function serverIsLive() {
|
||||
return livePids().length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* What npm installs from a manifest: its dependency lists, or the lockfile's
|
||||
* package tree — never the release's own `version`, which both files carry.
|
||||
* Hashing the whole files reinstalled every dependency on each update (minutes
|
||||
* on a slow link) when the bump changed nothing npm would fetch.
|
||||
*/
|
||||
function installedPart(file, text) {
|
||||
const json = JSON.parse(text);
|
||||
if (file === "package.json") {
|
||||
const { dependencies, optionalDependencies, overrides } = json;
|
||||
return JSON.stringify({ dependencies, optionalDependencies, overrides });
|
||||
}
|
||||
const { "": own, ...packages } = json.packages || {};
|
||||
const { version: _version, ...rest } = own || {};
|
||||
return JSON.stringify({ own: rest, packages });
|
||||
}
|
||||
|
||||
/** Hash of the dependency manifest — changes mean the runtime tree must be reinstalled. */
|
||||
function depsHash(root = PLUGIN_ROOT) {
|
||||
const h = crypto.createHash("sha256");
|
||||
for (const f of ["package.json", "package-lock.json"]) {
|
||||
let text;
|
||||
try {
|
||||
h.update(fs.readFileSync(path.join(root, f)));
|
||||
text = fs.readFileSync(path.join(root, f), "utf8");
|
||||
} catch {
|
||||
h.update(f); // absent counts as its own state
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
h.update(installedPart(f, text));
|
||||
} catch {
|
||||
h.update(text); // not JSON npm could read either: the bytes decide
|
||||
}
|
||||
}
|
||||
return h.digest("hex").slice(0, 16);
|
||||
@@ -422,13 +446,22 @@ function installDeps(rt = runtimeDir(), root = PLUGIN_ROOT) {
|
||||
if (fs.existsSync(src)) fs.copyFileSync(src, path.join(rt, f));
|
||||
}
|
||||
const npm = npmCommand(["install", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund"]);
|
||||
const res = spawnSync(npm.file, npm.args, {
|
||||
cwd: rt,
|
||||
stdio: "inherit",
|
||||
env: npm.env,
|
||||
shell: npm.shell,
|
||||
});
|
||||
return res.status === 0;
|
||||
// npm's output goes to a log: the worker's own stdout is discarded, so
|
||||
// inheriting it threw away the one explanation of a failed install.
|
||||
const logPath = path.join(rt, "npm-install.log");
|
||||
require("./log-rotate").rotateIfLarge(logPath);
|
||||
const logFd = fs.openSync(logPath, "a");
|
||||
try {
|
||||
const res = spawnSync(npm.file, npm.args, {
|
||||
cwd: rt,
|
||||
stdio: ["ignore", logFd, logFd],
|
||||
env: npm.env,
|
||||
shell: npm.shell,
|
||||
});
|
||||
return res.status === 0;
|
||||
} finally {
|
||||
fs.closeSync(logFd);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ server */
|
||||
@@ -695,7 +728,10 @@ function bootstrap(rt = runtimeDir(), { force = false } = {}) {
|
||||
if (force || state.depsHash !== wantDeps || !fs.existsSync(path.join(rt, "node_modules"))) {
|
||||
log(rt, "installing runtime dependencies (first run takes a few minutes)");
|
||||
if (!installDeps(rt)) {
|
||||
log(rt, "npm install failed — see the output above; run /agenthub-doctor after fixing it");
|
||||
log(
|
||||
rt,
|
||||
`npm install failed — see ${path.join(rt, "npm-install.log")}; run /agenthub-doctor after fixing it`
|
||||
);
|
||||
return "install-failed";
|
||||
}
|
||||
}
|
||||
@@ -713,13 +749,14 @@ function bootstrap(rt = runtimeDir(), { force = false } = {}) {
|
||||
|
||||
// Built eagerly (not lazily behind /agenthub-open) so the dashboard, including
|
||||
// client-only routes like /run, works the instant `claude` is started —
|
||||
// same trigger as the dependency install above: missing, or this plugin
|
||||
// version has not built one yet.
|
||||
// when missing, or when this plugin version has not built one yet
|
||||
// (`clientBuild` is recorded only after a build that did not fail, so the
|
||||
// next session retries a failed one instead of keeping the old UI for good).
|
||||
const { buildClient } = require("./plugin-open");
|
||||
const uiResult = buildClient({
|
||||
rt,
|
||||
root: PLUGIN_ROOT,
|
||||
force: force || state.depsHash !== wantDeps,
|
||||
force: force || state.clientBuild !== pluginVersion(),
|
||||
logPath: path.join(rt, "client-build.log"),
|
||||
});
|
||||
if (uiResult === "failed") {
|
||||
@@ -829,6 +866,7 @@ function bootstrap(rt = runtimeDir(), { force = false } = {}) {
|
||||
{
|
||||
pluginVersion: pluginVersion(),
|
||||
depsHash: wantDeps,
|
||||
clientBuild: uiResult === "failed" ? state.clientBuild : pluginVersion(),
|
||||
pluginRoot: PLUGIN_ROOT,
|
||||
claudeHome: getClaudeHome(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
|
||||
Reference in New Issue
Block a user