feat: v2.8.0 — Artifacts uploads and plugin links, console tab combos, account avatars

- Artifacts hub: upload a file from the computer; make a skill by uploading a
  Markdown file, files, a folder or a .zip/.skill, or by writing it in the
  dialog; add plugins by picking from the list a marketplace link answers
  (POST /api/artifacts/plugin/inspect); Settings → AI tools → Artifacts Hub.
- Console tabs are combos of up to four split panes (ConsoleTabs, PaneGrid).
- Accounts: an own avatar (PUT/DELETE /api/me/avatar, raster only, ≤ 1 MB,
  <data dir>/avatars/), shown with name and role at the sidebar's foot and on
  the workspace owner groups (GET /api/users/:id/avatar, owner_avatar_at).
- Users page: per-user storage (GET /api/users/storage, measured in a worker)
  and account-pool spend/tokens per account (server/lib/pool-usage.js).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 17:21:33 +07:00
parent 8ed5018a2f
commit 7a88b74016
63 changed files with 3833 additions and 354 deletions
+10 -5
View File
@@ -736,12 +736,17 @@ function inferredDisplayStage(lane) {
return lane.detected_stage;
}
/** The owner's username and display name — the staff sidebar groups
* workspaces by owner and shows the display name when one is set. */
/** The owner's username, display name and avatar version — the staff
* sidebar groups workspaces by owner and shows the display name and picture
* (`GET /api/users/:id/avatar`) when one is set. */
function ownerNames(id) {
if (id == null) return { owner_name: null, owner_display_name: null };
const u = db.prepare("SELECT username, display_name FROM users WHERE id = ?").get(id);
return { owner_name: u?.username ?? null, owner_display_name: u?.display_name ?? null };
if (id == null) return { owner_name: null, owner_display_name: null, owner_avatar_at: null };
const u = db.prepare("SELECT username, display_name, avatar_at FROM users WHERE id = ?").get(id);
return {
owner_name: u?.username ?? null,
owner_display_name: u?.display_name ?? null,
owner_avatar_at: u?.avatar_at ?? null,
};
}
function lanePayload(lane, ageSec = null) {