chore: v2.8.13 — Artifacts reinstall/edit/installs, agent steps in chat, pick a local file to attach, Windows paths as links

- Chat: an Agent step shows its task, status and tool calls and opens its conversation in place; Windows paths (E:\…, C:/…) in replies open as files
- Prompt: attach a file on this computer by path (native picker), clearer too-large messages
- Artifacts: edit title/description and a skill's files (new snapshot), Reinstall, install history, Hub-only rows, skills installed into ~/.claude/skills and ~/.agents/skills
- Files: batch upload of small files in one extract
- AI Tools: npm install falls back to --prefix ~/.local when the global prefix is not writable
This commit is contained in:
2026-10-20 16:44:55 +07:00
parent 49f87c4da3
commit 0547488f13
59 changed files with 4285 additions and 690 deletions
+26 -7
View File
@@ -46,13 +46,19 @@ function sendError(res, err) {
.json({ error: { code: err.code, message: err.message } });
}
console.warn("[artifacts]", err);
return res.status(500).json({ error: { code: "EINTERNAL", message: String(err?.message || err) } });
return res
.status(500)
.json({ error: { code: "EINTERNAL", message: String(err?.message || err) } });
}
/** Tell clients a row changed: everyone when shared, else its owner (+ staff). */
function changed(row) {
if (!row) return;
broadcast("artifact_changed", { id: row.id, ownerId: row.ownerId ?? row.owner_id ?? null, shared: !!(row.shared === true || row.shared === 1) });
broadcast("artifact_changed", {
id: row.id,
ownerId: row.ownerId ?? row.owner_id ?? null,
shared: !!(row.shared === true || row.shared === 1),
});
}
const wrap = (fn) => async (req, res) => {
@@ -101,7 +107,9 @@ router.put(
}
const on = req.body?.installsEnabled;
if (typeof on !== "boolean") {
return res.status(400).json({ error: { code: "EBADFIELD", message: "installsEnabled: boolean" } });
return res
.status(400)
.json({ error: { code: "EBADFIELD", message: "installsEnabled: boolean" } });
}
artifacts.setInstallsEnabled(on);
if (req.user) {
@@ -230,7 +238,9 @@ router.post(
wrap((req, res) => {
const f = (req.files || [])[0];
if (!f) {
return res.status(400).json({ error: { code: "ENOFILE", message: "attach the file as `files`" } });
return res
.status(400)
.json({ error: { code: "ENOFILE", message: "attach the file as `files`" } });
}
const item = artifacts.uploadFile(req, {
name: f.originalname,
@@ -264,7 +274,8 @@ router.patch(
wrap((req, res) => {
const item = artifacts.update(req, req.params.id, req.body || {});
changed(item);
res.json({ item });
// `removed`: unsharing a Hub-only row deleted it.
res.json(item.removed ? { item, removed: true } : { item });
})
);
@@ -274,7 +285,8 @@ router.delete(
wrap((req, res) => {
const row = artifacts.remove(req, req.params.id);
changed(row);
res.json({ ok: true });
// `kept`: a shared row left the owner's library but stays in the Hub.
res.json({ ok: true, kept: !!row.kept });
})
);
@@ -318,7 +330,8 @@ router.get(
const download = req.query.download === "1" || !type;
res.set({
"Content-Type": download ? "application/octet-stream" : type,
"Content-Security-Policy": "default-src 'none'; img-src data:; style-src 'unsafe-inline'; media-src 'self'; sandbox",
"Content-Security-Policy":
"default-src 'none'; img-src data:; style-src 'unsafe-inline'; media-src 'self'; sandbox",
"X-Content-Type-Options": "nosniff",
"Cache-Control": "private, max-age=60",
...(download
@@ -331,6 +344,12 @@ router.get(
})
);
// Who installed it, when and where (`artifacts.installsOf`).
router.get(
"/:id/installs",
wrap((req, res) => res.json(artifacts.installsOf(req, req.params.id)))
);
router.post(
"/:id/install",
sameOriginGuard,