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
@@ -1,7 +1,8 @@
/**
* @file WorkspaceTree.owners.test.tsx
* @description Server mode, the sidebar workspace list for an analyst: every
* user's workspaces, grouped under their owner (yours first, no owner last),
* user's workspaces, grouped under their owner (with their avatar, else an
* initial; yours first, no owner last),
* groups folding and remembered; another user's workspace is read only — no
* menu, no new session, and a session opens its analysis page, never a pane.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
@@ -30,7 +31,11 @@ const lane = (id: number, owner: number | null, name: string | null, title: stri
}) as unknown as Lane;
const lanes = [
{ ...lane(1, 3, "bob", "Bob app"), owner_display_name: "Bob Nguyen" } as Lane,
{
...lane(1, 3, "bob", "Bob app"),
owner_display_name: "Bob Nguyen",
owner_avatar_at: "t1",
} as Lane,
lane(2, null, null, "Old"),
lane(3, ME.id, "ana", "Mine"),
lane(4, 1, "admin", "Admin site"),
@@ -61,6 +66,11 @@ vi.mock("../../lib/api", async (importOriginal) => {
})),
},
run: { list: vi.fn(async () => ({ items: [] })) },
users: {
avatarUrl: (id: number, at?: string | null) =>
at ? `/api/users/${id}/avatar?v=${at}` : null,
},
me: { avatarUrl: () => null },
aiTools: { list: vi.fn(async () => ({ tools: [], default: "claude" })), iconUrl: () => "" },
},
};
@@ -118,6 +128,15 @@ describe("WorkspaceTree for an analyst", () => {
await screen.findByTestId("workspace-row-1");
expect(screen.getByTestId("workspace-owner-3")).toHaveTextContent("Bob Nguyen");
expect(screen.getByTestId("workspace-owner-3")).not.toHaveTextContent("bob");
// Bob set a picture: shown beside the name; Ana did not: her initial.
expect(
within(screen.getByTestId("workspace-owner-3"))
.getByTestId("user-avatar-img")
.getAttribute("src")
).toBe("/api/users/3/avatar?v=t1");
expect(
within(screen.getByTestId("workspace-owner-2")).getByTestId("user-avatar-initials")
).toHaveTextContent("a");
expect(screen.getByTestId("workspace-owner-1")).toHaveTextContent("admin");
});