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:
@@ -27,21 +27,33 @@ const pty = require("../lib/pty-run");
|
||||
const TOKEN = (c) => `sk-ant-oat01-${c.repeat(40)}`;
|
||||
const realFetchUsage = ccUsage.fetchUsage;
|
||||
|
||||
function run(id, account, { status = "completed", session = null, started } = {}) {
|
||||
function run(id, account, { status = "completed", session = null, started, ended = null } = {}) {
|
||||
db.prepare(
|
||||
`INSERT INTO dashboard_runs (id, session_id, mode, cwd, status, started_at, account)
|
||||
VALUES (?, ?, 'interactive', '/tmp', ?, ?, ?)`
|
||||
).run(id, session, status, started || new Date().toISOString(), account);
|
||||
`INSERT INTO dashboard_runs (id, session_id, mode, cwd, status, started_at, ended_at, account)
|
||||
VALUES (?, ?, 'interactive', '/tmp', ?, ?, ?, ?)`
|
||||
).run(id, session, status, started || new Date().toISOString(), ended, account);
|
||||
}
|
||||
|
||||
function spend(session, tokens) {
|
||||
const MODEL = "claude-sonnet-4-5-20250929";
|
||||
const hoursAgo = (h) => new Date(Date.now() - h * 3600e3).toISOString();
|
||||
|
||||
/**
|
||||
* `tokens` output tokens of `session` spent `atHoursAgo` hours ago — the
|
||||
* hourly row plus the session total, as the hook writer keeps them both.
|
||||
* `{hourly: false}` = a total with no hourly row (a sub-agent's bucket).
|
||||
*/
|
||||
function spend(session, tokens, { atHoursAgo = 0, hourly = true, model = MODEL } = {}) {
|
||||
db.prepare(
|
||||
"INSERT OR IGNORE INTO sessions (id, name, status, cwd) VALUES (?, ?, 'completed', '/tmp')"
|
||||
).run(session, session);
|
||||
"INSERT OR IGNORE INTO sessions (id, name, status, cwd, started_at) VALUES (?, ?, 'completed', '/tmp', ?)"
|
||||
).run(session, session, hoursAgo(atHoursAgo));
|
||||
db.prepare(
|
||||
`INSERT INTO token_usage_hourly (session_id, hour, model, output_tokens)
|
||||
VALUES (?, ?, 'claude-sonnet-4-5-20250929', ?)`
|
||||
).run(session, new Date().toISOString().slice(0, 13), tokens);
|
||||
`INSERT INTO token_usage (session_id, model, output_tokens) VALUES (?, ?, ?)
|
||||
ON CONFLICT DO UPDATE SET output_tokens = output_tokens + excluded.output_tokens`
|
||||
).run(session, model, tokens);
|
||||
if (!hourly) return;
|
||||
db.prepare(
|
||||
`INSERT INTO token_usage_hourly (session_id, hour, model, output_tokens) VALUES (?, ?, ?, ?)`
|
||||
).run(session, hoursAgo(atHoursAgo).slice(0, 13), model, tokens);
|
||||
}
|
||||
|
||||
before(() => {
|
||||
@@ -62,6 +74,8 @@ describe("claude pool", () => {
|
||||
fs.rmSync(pool.poolDir(), { recursive: true, force: true });
|
||||
db.prepare("DELETE FROM dashboard_runs").run();
|
||||
db.prepare("DELETE FROM token_usage_hourly").run();
|
||||
db.prepare("DELETE FROM token_usage").run();
|
||||
db.prepare("DELETE FROM sessions").run();
|
||||
pool.__quotaCache.clear();
|
||||
});
|
||||
|
||||
@@ -109,6 +123,26 @@ describe("claude pool", () => {
|
||||
assert.equal(pool.pickAccount().name, "b");
|
||||
});
|
||||
|
||||
it("books a session to an account only for the hours its runs held it, sub-agents included", () => {
|
||||
for (const n of ["a", "b"]) pool.addAccount(n, TOKEN(n));
|
||||
// s1 ran on a 30 h ago, then was resumed on b 2 h ago.
|
||||
run("r-a", "a", { session: "s1", started: hoursAgo(30), ended: hoursAgo(29) });
|
||||
run("r-b", "b", { session: "s1", started: hoursAgo(2), status: "running" });
|
||||
spend("s1", 1000, { atHoursAgo: 30 });
|
||||
spend("s1", 3000, { atHoursAgo: 2 });
|
||||
// s2 (on a, now): its main agent and a Haiku sub-agent with no hourly row.
|
||||
run("r-a2", "a", { session: "s2" });
|
||||
spend("s2", 500);
|
||||
spend("s2", 200, { hourly: false, model: "claude-haiku-4-5-20251001" });
|
||||
const status = Object.fromEntries(pool.poolStatus().map((x) => [x.name, x]));
|
||||
assert.equal(status.a.tokens5h, 700, "s2, sub-agent included; s1 is b's now");
|
||||
assert.equal(status.a.tokens7d, 1700);
|
||||
assert.equal(status.b.tokens5h, 3000, "not the hours a held s1");
|
||||
assert.equal(status.b.tokens7d, 3000);
|
||||
assert.ok(status.a.spend7d > status.a.spend5h && status.a.spend5h > 0);
|
||||
assert.ok(status.b.spend5h > 0);
|
||||
});
|
||||
|
||||
it("puts an account whose quota is nearly used up last", async () => {
|
||||
for (const n of ["a", "b"]) pool.addAccount(n, TOKEN(n));
|
||||
ccUsage.fetchUsage = async (token) => ({
|
||||
|
||||
Reference in New Issue
Block a user