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:
@@ -91,6 +91,34 @@ describe("node version gate", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("depsHash — only what npm installs", () => {
|
||||
function root(pkg, lock) {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-deps-"));
|
||||
fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify(pkg));
|
||||
fs.writeFileSync(path.join(dir, "package-lock.json"), JSON.stringify(lock));
|
||||
return dir;
|
||||
}
|
||||
const pkg = (version, deps) => ({ name: "agenthub", version, dependencies: deps });
|
||||
const lock = (version, deps) => ({
|
||||
name: "agenthub",
|
||||
version,
|
||||
packages: { "": { name: "agenthub", version, dependencies: deps }, "node_modules/a": {} },
|
||||
});
|
||||
|
||||
it("ignores a release's version bump", () => {
|
||||
// Every update reinstalled all runtime dependencies when only the version moved.
|
||||
const a = root(pkg("1.0.0", { a: "^1" }), lock("1.0.0", { a: "^1" }));
|
||||
const b = root(pkg("1.0.1", { a: "^1" }), lock("1.0.1", { a: "^1" }));
|
||||
assert.equal(boot.depsHash(a), boot.depsHash(b));
|
||||
});
|
||||
|
||||
it("changes when a dependency does", () => {
|
||||
const a = root(pkg("1.0.0", { a: "^1" }), lock("1.0.0", { a: "^1" }));
|
||||
const b = root(pkg("1.0.0", { a: "^2" }), lock("1.0.0", { a: "^2" }));
|
||||
assert.notEqual(boot.depsHash(a), boot.depsHash(b));
|
||||
});
|
||||
});
|
||||
|
||||
describe("fast path", () => {
|
||||
beforeEach(resetRuntime);
|
||||
|
||||
@@ -527,7 +555,6 @@ describe("buildClient dependency install", () => {
|
||||
assert.deepEqual(calls, [
|
||||
"ci --no-audit --no-fund --include=dev --ignore-scripts",
|
||||
"install --no-audit --no-fund --include=dev --ignore-scripts",
|
||||
"node tsc -b",
|
||||
"node vite.js build",
|
||||
]);
|
||||
});
|
||||
@@ -567,8 +594,9 @@ describe("buildClient dependency install", () => {
|
||||
it("builds with the running node, no lifecycle script and no PATH lookup (Windows)", () => {
|
||||
// On Windows a script or a .bin shim runs through cmd.exe and needs `node`
|
||||
// on PATH; the server's PATH lacked it, so esbuild's postinstall failed and
|
||||
// every update kept the previous UI. The build here must equal the
|
||||
// client's own "build" script, run without npm.
|
||||
// every update kept the previous UI. The build here is the vite half of
|
||||
// the client's own "build" script, run without npm; the type check is the
|
||||
// release's job, not every user's.
|
||||
assert.equal(
|
||||
JSON.parse(fs.readFileSync(path.join(__dirname, "..", "..", "client", "package.json")))
|
||||
.scripts.build,
|
||||
@@ -577,8 +605,35 @@ describe("buildClient dependency install", () => {
|
||||
resetRuntime();
|
||||
const calls = [];
|
||||
buildClient({ rt: RT, root: fakeRoot(), exec: fakeExec(calls) });
|
||||
assert.deepEqual(calls.slice(-2), ["node tsc -b", "node vite.js build"]);
|
||||
assert.ok(calls.every((c) => !c.startsWith("run")));
|
||||
assert.deepEqual(calls.slice(-1), ["node vite.js build"]);
|
||||
assert.ok(calls.every((c) => !c.startsWith("run") && !c.includes("tsc")));
|
||||
});
|
||||
|
||||
it("keeps the installed tree while the client's manifests are unchanged", () => {
|
||||
resetRuntime();
|
||||
const root = fakeRoot();
|
||||
const first = [];
|
||||
buildClient({ rt: RT, root, exec: fakeExec(first) });
|
||||
assert.ok(first[0].startsWith("ci"));
|
||||
const again = [];
|
||||
assert.equal(buildClient({ rt: RT, root, force: true, exec: fakeExec(again) }), "built");
|
||||
assert.deepEqual(again, ["node vite.js build"]);
|
||||
|
||||
fs.writeFileSync(path.join(root, "client", "package.json"), '{"dependencies":{"x":"1"}}');
|
||||
const changed = [];
|
||||
buildClient({ rt: RT, root, force: true, exec: fakeExec(changed) });
|
||||
assert.ok(changed[0].startsWith("ci"), "a changed manifest installs from clean");
|
||||
});
|
||||
|
||||
it("installs from clean again after a build on the kept tree failed", () => {
|
||||
resetRuntime();
|
||||
const root = fakeRoot();
|
||||
buildClient({ rt: RT, root, exec: fakeExec([]) });
|
||||
const failing = (cmd, args, cwd) => (cmd === "node" ? false : fakeExec([])(cmd, args, cwd));
|
||||
assert.equal(buildClient({ rt: RT, root, force: true, exec: failing }), "failed");
|
||||
const next = [];
|
||||
buildClient({ rt: RT, root, force: true, exec: fakeExec(next) });
|
||||
assert.ok(next[0].startsWith("ci"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+30
-7
@@ -33,6 +33,20 @@ if (process.env.GIT_EDITOR === "true") delete process.env.GIT_EDITOR;
|
||||
// server/lib/settings-env.js. Only when this
|
||||
// file IS the process: the test suite requires it dozens of times, and a
|
||||
// developer's real settings file pins DASHBOARD_DATA_DIR at their live database.
|
||||
/** What to tell the user when the dashboard cannot bind its port. The health
|
||||
* probe above already turned away a port held by a running dashboard, so a
|
||||
* port in use here belongs to some other program. */
|
||||
function listenFailure(err, port) {
|
||||
if (err?.code === "EADDRINUSE")
|
||||
return (
|
||||
`Port ${port} is already in use by another program, so AgentHub cannot start there. ` +
|
||||
`Stop that program, or run AgentHub on another port with DASHBOARD_PORT (for example DASHBOARD_PORT=4821).`
|
||||
);
|
||||
if (err?.code === "EACCES")
|
||||
return `Not allowed to listen on port ${port}. Pick a port above 1024 with DASHBOARD_PORT.`;
|
||||
return `AgentHub could not start its server: ${err?.stack || err}`;
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const settingsEnv = require("./lib/settings-env");
|
||||
settingsEnv.loadEnvFiles({
|
||||
@@ -337,8 +351,12 @@ function startServer(app, port) {
|
||||
const host = resolveHost();
|
||||
const boundLoopback = isLoopbackHostname(host);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// A port another program holds fails here, not as an uncaught 'error'
|
||||
// event: the caller says what happened instead of a bare stack trace.
|
||||
server.once("error", reject);
|
||||
server.listen(port, host, () => {
|
||||
server.off("error", reject);
|
||||
// Publish the live port so the Claude Code hook handler can find this
|
||||
// server even when it bound a non-default port (the desktop app falls
|
||||
// back off 4820 when that port is already taken).
|
||||
@@ -1210,12 +1228,17 @@ if (require.main === module) {
|
||||
return;
|
||||
}
|
||||
const app = createApp();
|
||||
startServer(app, PORT).then((server) => {
|
||||
httpServer = server;
|
||||
startBackgroundServices();
|
||||
// The status-area icon (server/lib/tray.js) — production servers only.
|
||||
require("./lib/tray").startTrayWatch();
|
||||
});
|
||||
startServer(app, PORT)
|
||||
.then((server) => {
|
||||
httpServer = server;
|
||||
startBackgroundServices();
|
||||
// The status-area icon (server/lib/tray.js) — production servers only.
|
||||
require("./lib/tray").startTrayWatch();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(listenFailure(err, PORT));
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Graceful shutdown — close connections and DB cleanly
|
||||
|
||||
Reference in New Issue
Block a user