Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f8dc9082e | |||
| f1a90e120e |
@@ -1,6 +1,6 @@
|
||||
# Claude Code Working Guide
|
||||
|
||||
_Last synced: v2.8.37 (2026-10-29) — the Artifacts page's installs list floats over the page, never clipped._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
|
||||
_Last synced: v2.8.38 (2026-10-29) — every user row (CPU/RAM card, editor, pools) shows its avatar._ A change that makes any statement in this file, `AGENTS.md`, or a README wrong — a moved path, a new or changed extension point, a retired rule — updates that file in the same change-set and bumps this marker. **Bump = replace the version/date and a ≤ 1-line summary; never append a changelog here** — details belong in the commit message and `docs/`, because this whole file is loaded into every session.
|
||||
|
||||
## Project mission
|
||||
- Maintain a reliable local-first dashboard for Claude Code session monitoring.
|
||||
|
||||
@@ -683,7 +683,12 @@ export function HostSections({ host, heap }: { host: HostState; heap?: HeapInfo
|
||||
aria-expanded={isOpen}
|
||||
className="flex w-full items-center gap-2 text-left disabled:cursor-default"
|
||||
>
|
||||
<UserAvatar name={name} className="h-6 w-6 text-[10px]" />
|
||||
<UserAvatar
|
||||
name={name}
|
||||
userId={g.userId}
|
||||
avatarAt={g.avatarAt}
|
||||
className="h-6 w-6 text-[10px]"
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate text-xs text-fg-secondary">
|
||||
{name}
|
||||
</span>
|
||||
|
||||
@@ -69,6 +69,7 @@ const stats: HostStats = {
|
||||
const api = vi.hoisted(() => ({
|
||||
host: { stats: vi.fn(), history: vi.fn() },
|
||||
me: { avatarUrl: () => null },
|
||||
users: { avatarUrl: () => null },
|
||||
}));
|
||||
vi.mock("../../../lib/api", async (importOriginal) => {
|
||||
const actual = await importOriginal<Record<string, unknown>>();
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Search } from "lucide-react";
|
||||
import type { AuthUser, ServerUserUsage } from "../../lib/api";
|
||||
import { Checkbox } from "../Checkbox";
|
||||
import { UserAvatar } from "./UserAvatar";
|
||||
|
||||
export const ROLES: AuthUser["role"][] = ["admin", "developer", "analyst"];
|
||||
/** A role, every user, or the users in no pool group. */
|
||||
@@ -168,7 +169,14 @@ export function MemberPicker({
|
||||
checked={chosen.has(u.id)}
|
||||
onChange={(on) => setMany([u.id], on)}
|
||||
label={
|
||||
<span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<UserAvatar
|
||||
name={u.displayName || u.username}
|
||||
userId={u.id}
|
||||
avatarAt={u.avatarAt}
|
||||
letters={1}
|
||||
className="h-5 w-5 text-[10px]"
|
||||
/>
|
||||
{u.username}
|
||||
{u.disabled && <span className="ml-1 text-fg-muted">({t("disabled")})</span>}
|
||||
{note?.(u)}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { toast } from "../../lib/toast";
|
||||
import { ConfirmModal } from "../ConfirmModal";
|
||||
import { AiToolIcon } from "../AiToolIcon";
|
||||
import { MemberPicker } from "./MemberPicker";
|
||||
import { UserAvatar } from "./UserAvatar";
|
||||
|
||||
/** Pool tools with their labels, in display order. */
|
||||
export const POOL_TOOL_LABELS: { id: PoolTool; label: string }[] = [
|
||||
@@ -363,12 +364,13 @@ export function PoolGroups({
|
||||
} ${isDragged(drag) ? "opacity-40" : ""}`}
|
||||
data-testid="pool-member"
|
||||
>
|
||||
<span
|
||||
className="flex h-4 w-4 items-center justify-center rounded-full bg-accent/15 text-[9px] font-semibold uppercase text-accent"
|
||||
aria-hidden
|
||||
>
|
||||
{name.charAt(0)}
|
||||
</span>
|
||||
<UserAvatar
|
||||
name={name}
|
||||
userId={id}
|
||||
avatarAt={u?.avatarAt}
|
||||
letters={1}
|
||||
className="h-4 w-4 text-[9px]"
|
||||
/>
|
||||
{name}
|
||||
{u && <span className="text-[10px] text-fg-muted">{t(`roles.${u.role}`)}</span>}
|
||||
{isAdmin && from && (
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
* (`GET /api/me/avatar`) or, with `userId`, another's (`GET
|
||||
* /api/users/:id/avatar`, staff only), versioned by `avatarAt`; without one —
|
||||
* or when the image fails to load — the first letters of its name. Used by the
|
||||
* sidebar's account row, its workspace owner groups and Settings → Account.
|
||||
* sidebar's account row, its workspace owner groups, Settings → Account, the
|
||||
* Users page and its editor, the pool board and member picker, and System
|
||||
* Health's per-user CPU/RAM card.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import { copyText } from "../../lib/clipboard";
|
||||
import { isSubmitEnter } from "../../lib/keys";
|
||||
import { passwordProblems } from "../../lib/passwordRules";
|
||||
import { PasswordRules } from "./PasswordRules";
|
||||
import { UserAvatar } from "./UserAvatar";
|
||||
import { useDialog } from "../../hooks/useDialog";
|
||||
import { Switch } from "../Switch";
|
||||
import { Checkbox } from "../Checkbox";
|
||||
@@ -243,12 +244,12 @@ export function UserEditDialog({
|
||||
className="flex max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl border border-border bg-surface-1 shadow-xl"
|
||||
>
|
||||
<div className="flex items-start gap-3 border-b border-border px-5 py-4">
|
||||
<span
|
||||
aria-hidden
|
||||
className="flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-full bg-accent/15 text-sm font-semibold uppercase text-accent"
|
||||
>
|
||||
{shown.slice(0, 2)}
|
||||
</span>
|
||||
<UserAvatar
|
||||
name={shown}
|
||||
userId={user.id}
|
||||
avatarAt={user.avatarAt}
|
||||
className="h-10 w-10 text-sm"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h2 id="user-edit-title" className="text-base font-semibold text-fg-primary">
|
||||
{t("edit.title", { name: user.displayName || user.username })}
|
||||
|
||||
@@ -33,7 +33,7 @@ const accountPool = vi.hoisted(() => ({
|
||||
updateGroup: vi.fn(),
|
||||
deleteGroup: vi.fn(),
|
||||
}));
|
||||
const users = vi.hoisted(() => ({ list: vi.fn() }));
|
||||
const users = vi.hoisted(() => ({ list: vi.fn(), avatarUrl: () => null }));
|
||||
const aiTools = vi.hoisted(() => ({
|
||||
list: vi.fn(),
|
||||
ompSharing: vi.fn(),
|
||||
|
||||
@@ -13,7 +13,12 @@ import { render, screen, waitFor } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import type { ServerUserUsage } from "../../../lib/api";
|
||||
|
||||
const users = vi.hoisted(() => ({ get: vi.fn(), update: vi.fn(), remove: vi.fn() }));
|
||||
const users = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
update: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
avatarUrl: () => null,
|
||||
}));
|
||||
const confirmDialog = vi.hoisted(() => vi.fn());
|
||||
vi.mock("../../../lib/confirm", () => ({ confirmDialog }));
|
||||
const lanes = vi.hoisted(() => ({ list: vi.fn() }));
|
||||
|
||||
@@ -16,6 +16,7 @@ const users = vi.hoisted(() => ({
|
||||
setQuotaDefault: vi.fn(),
|
||||
update: vi.fn(),
|
||||
get: vi.fn(),
|
||||
avatarUrl: () => null,
|
||||
}));
|
||||
const lanes = vi.hoisted(() => ({ list: vi.fn() }));
|
||||
vi.mock("../../../lib/api", async (importOriginal) => {
|
||||
|
||||
@@ -684,6 +684,8 @@ export interface HostUsageGroup {
|
||||
username?: string;
|
||||
/** The user's display name, null when none is set (absent from an older server). */
|
||||
displayName?: string | null;
|
||||
/** When the user's avatar was last set (null = none; absent from an older server). */
|
||||
avatarAt?: string | null;
|
||||
/** % of the whole machine's CPU (100 = every core busy) over the sample. */
|
||||
cpu: number;
|
||||
/** Resident memory, bytes (shared pages count in every process). */
|
||||
|
||||
@@ -7316,7 +7316,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
||||
<span
|
||||
class="text-status-warning/90"
|
||||
>
|
||||
UI build v2.8.37 (rebuild client to match)
|
||||
UI build v2.8.38 (rebuild client to match)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "agenthub",
|
||||
"version": "2.8.37",
|
||||
"version": "2.8.38",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "agenthub",
|
||||
"version": "2.8.37",
|
||||
"version": "2.8.38",
|
||||
"hasInstallScript": true,
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agenthub",
|
||||
"version": "2.8.37",
|
||||
"version": "2.8.38",
|
||||
"bin": {
|
||||
"agenthub": "bin/agenthub.js"
|
||||
},
|
||||
|
||||
@@ -555,12 +555,13 @@ async function usage(t0, t1, p0, p1, mem) {
|
||||
|
||||
const round = (n) => Math.round(n * 10) / 10;
|
||||
|
||||
/** A user's group fields: id, username and display name (every viewer reads them). */
|
||||
/** A user's group fields: id, username, display name and avatar version (every viewer reads them). */
|
||||
const userExtra = (u) => ({
|
||||
kind: "user",
|
||||
userId: u.id,
|
||||
username: u.username,
|
||||
displayName: u.displayName ?? null,
|
||||
avatarAt: u.avatarAt ?? null,
|
||||
});
|
||||
|
||||
async function collect() {
|
||||
|
||||
Reference in New Issue
Block a user