Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f8dc9082e | |||
| f1a90e120e | |||
| db5a7776cc | |||
| 6c2c094f64 |
@@ -1,6 +1,6 @@
|
|||||||
# Claude Code Working Guide
|
# Claude Code Working Guide
|
||||||
|
|
||||||
_Last synced: v2.8.36 (2026-10-29) — an HTML page opens in a modal over the screen (Changes Preview, Chat)._ 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
|
## Project mission
|
||||||
- Maintain a reliable local-first dashboard for Claude Code session monitoring.
|
- 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}
|
aria-expanded={isOpen}
|
||||||
className="flex w-full items-center gap-2 text-left disabled:cursor-default"
|
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">
|
<span className="min-w-0 flex-1 truncate text-xs text-fg-secondary">
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ const stats: HostStats = {
|
|||||||
const api = vi.hoisted(() => ({
|
const api = vi.hoisted(() => ({
|
||||||
host: { stats: vi.fn(), history: vi.fn() },
|
host: { stats: vi.fn(), history: vi.fn() },
|
||||||
me: { avatarUrl: () => null },
|
me: { avatarUrl: () => null },
|
||||||
|
users: { avatarUrl: () => null },
|
||||||
}));
|
}));
|
||||||
vi.mock("../../../lib/api", async (importOriginal) => {
|
vi.mock("../../../lib/api", async (importOriginal) => {
|
||||||
const actual = await importOriginal<Record<string, unknown>>();
|
const actual = await importOriginal<Record<string, unknown>>();
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
import type { AuthUser, ServerUserUsage } from "../../lib/api";
|
import type { AuthUser, ServerUserUsage } from "../../lib/api";
|
||||||
import { Checkbox } from "../Checkbox";
|
import { Checkbox } from "../Checkbox";
|
||||||
|
import { UserAvatar } from "./UserAvatar";
|
||||||
|
|
||||||
export const ROLES: AuthUser["role"][] = ["admin", "developer", "analyst"];
|
export const ROLES: AuthUser["role"][] = ["admin", "developer", "analyst"];
|
||||||
/** A role, every user, or the users in no pool group. */
|
/** A role, every user, or the users in no pool group. */
|
||||||
@@ -168,7 +169,14 @@ export function MemberPicker({
|
|||||||
checked={chosen.has(u.id)}
|
checked={chosen.has(u.id)}
|
||||||
onChange={(on) => setMany([u.id], on)}
|
onChange={(on) => setMany([u.id], on)}
|
||||||
label={
|
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.username}
|
||||||
{u.disabled && <span className="ml-1 text-fg-muted">({t("disabled")})</span>}
|
{u.disabled && <span className="ml-1 text-fg-muted">({t("disabled")})</span>}
|
||||||
{note?.(u)}
|
{note?.(u)}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import { toast } from "../../lib/toast";
|
|||||||
import { ConfirmModal } from "../ConfirmModal";
|
import { ConfirmModal } from "../ConfirmModal";
|
||||||
import { AiToolIcon } from "../AiToolIcon";
|
import { AiToolIcon } from "../AiToolIcon";
|
||||||
import { MemberPicker } from "./MemberPicker";
|
import { MemberPicker } from "./MemberPicker";
|
||||||
|
import { UserAvatar } from "./UserAvatar";
|
||||||
|
|
||||||
/** Pool tools with their labels, in display order. */
|
/** Pool tools with their labels, in display order. */
|
||||||
export const POOL_TOOL_LABELS: { id: PoolTool; label: string }[] = [
|
export const POOL_TOOL_LABELS: { id: PoolTool; label: string }[] = [
|
||||||
@@ -363,12 +364,13 @@ export function PoolGroups({
|
|||||||
} ${isDragged(drag) ? "opacity-40" : ""}`}
|
} ${isDragged(drag) ? "opacity-40" : ""}`}
|
||||||
data-testid="pool-member"
|
data-testid="pool-member"
|
||||||
>
|
>
|
||||||
<span
|
<UserAvatar
|
||||||
className="flex h-4 w-4 items-center justify-center rounded-full bg-accent/15 text-[9px] font-semibold uppercase text-accent"
|
name={name}
|
||||||
aria-hidden
|
userId={id}
|
||||||
>
|
avatarAt={u?.avatarAt}
|
||||||
{name.charAt(0)}
|
letters={1}
|
||||||
</span>
|
className="h-4 w-4 text-[9px]"
|
||||||
|
/>
|
||||||
{name}
|
{name}
|
||||||
{u && <span className="text-[10px] text-fg-muted">{t(`roles.${u.role}`)}</span>}
|
{u && <span className="text-[10px] text-fg-muted">{t(`roles.${u.role}`)}</span>}
|
||||||
{isAdmin && from && (
|
{isAdmin && from && (
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
* (`GET /api/me/avatar`) or, with `userId`, another's (`GET
|
* (`GET /api/me/avatar`) or, with `userId`, another's (`GET
|
||||||
* /api/users/:id/avatar`, staff only), versioned by `avatarAt`; without one —
|
* /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
|
* 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>
|
* @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 { isSubmitEnter } from "../../lib/keys";
|
||||||
import { passwordProblems } from "../../lib/passwordRules";
|
import { passwordProblems } from "../../lib/passwordRules";
|
||||||
import { PasswordRules } from "./PasswordRules";
|
import { PasswordRules } from "./PasswordRules";
|
||||||
|
import { UserAvatar } from "./UserAvatar";
|
||||||
import { useDialog } from "../../hooks/useDialog";
|
import { useDialog } from "../../hooks/useDialog";
|
||||||
import { Switch } from "../Switch";
|
import { Switch } from "../Switch";
|
||||||
import { Checkbox } from "../Checkbox";
|
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"
|
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">
|
<div className="flex items-start gap-3 border-b border-border px-5 py-4">
|
||||||
<span
|
<UserAvatar
|
||||||
aria-hidden
|
name={shown}
|
||||||
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"
|
userId={user.id}
|
||||||
>
|
avatarAt={user.avatarAt}
|
||||||
{shown.slice(0, 2)}
|
className="h-10 w-10 text-sm"
|
||||||
</span>
|
/>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h2 id="user-edit-title" className="text-base font-semibold text-fg-primary">
|
<h2 id="user-edit-title" className="text-base font-semibold text-fg-primary">
|
||||||
{t("edit.title", { name: user.displayName || user.username })}
|
{t("edit.title", { name: user.displayName || user.username })}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const accountPool = vi.hoisted(() => ({
|
|||||||
updateGroup: vi.fn(),
|
updateGroup: vi.fn(),
|
||||||
deleteGroup: 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(() => ({
|
const aiTools = vi.hoisted(() => ({
|
||||||
list: vi.fn(),
|
list: vi.fn(),
|
||||||
ompSharing: 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 userEvent from "@testing-library/user-event";
|
||||||
import type { ServerUserUsage } from "../../../lib/api";
|
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());
|
const confirmDialog = vi.hoisted(() => vi.fn());
|
||||||
vi.mock("../../../lib/confirm", () => ({ confirmDialog }));
|
vi.mock("../../../lib/confirm", () => ({ confirmDialog }));
|
||||||
const lanes = vi.hoisted(() => ({ list: vi.fn() }));
|
const lanes = vi.hoisted(() => ({ list: vi.fn() }));
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const users = vi.hoisted(() => ({
|
|||||||
setQuotaDefault: vi.fn(),
|
setQuotaDefault: vi.fn(),
|
||||||
update: vi.fn(),
|
update: vi.fn(),
|
||||||
get: vi.fn(),
|
get: vi.fn(),
|
||||||
|
avatarUrl: () => null,
|
||||||
}));
|
}));
|
||||||
const lanes = vi.hoisted(() => ({ list: vi.fn() }));
|
const lanes = vi.hoisted(() => ({ list: vi.fn() }));
|
||||||
vi.mock("../../../lib/api", async (importOriginal) => {
|
vi.mock("../../../lib/api", async (importOriginal) => {
|
||||||
|
|||||||
@@ -684,6 +684,8 @@ export interface HostUsageGroup {
|
|||||||
username?: string;
|
username?: string;
|
||||||
/** The user's display name, null when none is set (absent from an older server). */
|
/** The user's display name, null when none is set (absent from an older server). */
|
||||||
displayName?: string | null;
|
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. */
|
/** % of the whole machine's CPU (100 = every core busy) over the sample. */
|
||||||
cpu: number;
|
cpu: number;
|
||||||
/** Resident memory, bytes (shared pages count in every process). */
|
/** Resident memory, bytes (shared pages count in every process). */
|
||||||
|
|||||||
@@ -48,7 +48,8 @@
|
|||||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -1629,12 +1630,41 @@ function DetailDialog({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** "N installs" on a card; a click lists who installed it, when, and where. */
|
/** "N installs" on a card; a click lists who installed it, when, and where.
|
||||||
|
* The list floats over the page (a portal at the button, below it or above
|
||||||
|
* when there is no room, kept inside the window): inside the card list's
|
||||||
|
* `overflow-hidden` it was cut off at the last card. */
|
||||||
function InstallsButton({ item }: { item: Artifact }) {
|
function InstallsButton({ item }: { item: Artifact }) {
|
||||||
const { t } = useTranslation("artifacts");
|
const { t } = useTranslation("artifacts");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [data, setData] = useState<ArtifactInstalls | null>(null);
|
const [data, setData] = useState<ArtifactInstalls | null>(null);
|
||||||
const box = useRef<HTMLSpanElement>(null);
|
const box = useRef<HTMLSpanElement>(null);
|
||||||
|
const pop = useRef<HTMLDivElement>(null);
|
||||||
|
const [at, setAt] = useState<{ left: number; top: number } | null>(null);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!open) return setAt(null);
|
||||||
|
const place = () => {
|
||||||
|
const b = box.current?.getBoundingClientRect();
|
||||||
|
const p = pop.current;
|
||||||
|
if (!b || !p) return;
|
||||||
|
const w = p.offsetWidth;
|
||||||
|
const h = p.offsetHeight;
|
||||||
|
const gap = 4;
|
||||||
|
const below = b.bottom + gap + h <= window.innerHeight - 8 || b.top - gap - h < 8;
|
||||||
|
setAt({
|
||||||
|
left: Math.max(8, Math.min(b.left, window.innerWidth - w - 8)),
|
||||||
|
top: below ? b.bottom + gap : b.top - gap - h,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
place();
|
||||||
|
window.addEventListener("resize", place);
|
||||||
|
window.addEventListener("scroll", place, true);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", place);
|
||||||
|
window.removeEventListener("scroll", place, true);
|
||||||
|
};
|
||||||
|
}, [open, data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
@@ -1647,7 +1677,8 @@ function InstallsButton({ item }: { item: Artifact }) {
|
|||||||
toast.error(err);
|
toast.error(err);
|
||||||
});
|
});
|
||||||
const onDown = (e: MouseEvent) => {
|
const onDown = (e: MouseEvent) => {
|
||||||
if (!box.current?.contains(e.target as Node)) setOpen(false);
|
const n = e.target as Node;
|
||||||
|
if (!box.current?.contains(n) && !pop.current?.contains(n)) setOpen(false);
|
||||||
};
|
};
|
||||||
const onKey = (e: KeyboardEvent) => {
|
const onKey = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") setOpen(false);
|
if (e.key === "Escape") setOpen(false);
|
||||||
@@ -1670,66 +1701,70 @@ function InstallsButton({ item }: { item: Artifact }) {
|
|||||||
>
|
>
|
||||||
{t("installs", { count: item.installs })}
|
{t("installs", { count: item.installs })}
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open &&
|
||||||
<div
|
createPortal(
|
||||||
role="dialog"
|
<div
|
||||||
aria-label={t("installsTitle")}
|
ref={pop}
|
||||||
data-testid="artifact-installs"
|
role="dialog"
|
||||||
className="absolute left-0 z-30 mt-1 w-80 max-w-[80vw] rounded-lg border border-border bg-surface-1 p-2 shadow-xl"
|
aria-label={t("installsTitle")}
|
||||||
>
|
data-testid="artifact-installs"
|
||||||
<p className="mb-1.5 px-1 text-xs font-medium text-fg-primary">{t("installsTitle")}</p>
|
style={at ? { left: at.left, top: at.top } : { left: 0, top: 0, visibility: "hidden" }}
|
||||||
{data === null ? (
|
className="fixed z-[60] w-80 max-w-[80vw] rounded-lg border border-border bg-surface-1 p-2 shadow-xl"
|
||||||
<p className="px-1 text-xs text-fg-muted">{t("loading")}</p>
|
>
|
||||||
) : (
|
<p className="mb-1.5 px-1 text-xs font-medium text-fg-primary">{t("installsTitle")}</p>
|
||||||
<ul className="max-h-64 space-y-1 overflow-auto">
|
{data === null ? (
|
||||||
{data.items.map((i, n) => (
|
<p className="px-1 text-xs text-fg-muted">{t("loading")}</p>
|
||||||
<li key={n} className="rounded px-1.5 py-1 text-xs hover:bg-surface-2">
|
) : (
|
||||||
<div className="flex items-center gap-2">
|
<ul className="max-h-64 space-y-1 overflow-auto">
|
||||||
<span className="min-w-0 flex-1 truncate text-fg-primary">
|
{data.items.map((i, n) => (
|
||||||
{i.mine ? t("installsYou") : (i.user ?? t("installsUnknown"))}
|
<li key={n} className="rounded px-1.5 py-1 text-xs hover:bg-surface-2">
|
||||||
{i.replaced && (
|
<div className="flex items-center gap-2">
|
||||||
<span className="ml-1 text-fg-muted">· {t("installsReplaced")}</span>
|
<span className="min-w-0 flex-1 truncate text-fg-primary">
|
||||||
)}
|
{i.mine ? t("installsYou") : (i.user ?? t("installsUnknown"))}
|
||||||
</span>
|
{i.replaced && (
|
||||||
<span className="shrink-0 text-[11px] text-fg-muted">
|
<span className="ml-1 text-fg-muted">· {t("installsReplaced")}</span>
|
||||||
{new Date(i.at).toLocaleString()}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
<span className="shrink-0 text-[11px] text-fg-muted">
|
||||||
{i.paths && (
|
{new Date(i.at).toLocaleString()}
|
||||||
<div className="mt-0.5 space-y-0.5">
|
</span>
|
||||||
{i.paths.length ? (
|
|
||||||
i.paths.map((p) => (
|
|
||||||
<p
|
|
||||||
key={p}
|
|
||||||
className="truncate font-mono text-[10px] text-fg-muted"
|
|
||||||
title={p}
|
|
||||||
>
|
|
||||||
{p}
|
|
||||||
</p>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<p className="font-mono text-[10px] text-fg-muted">
|
|
||||||
{t("installsPluginWhere")}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
{i.paths && (
|
||||||
</li>
|
<div className="mt-0.5 space-y-0.5">
|
||||||
))}
|
{i.paths.length ? (
|
||||||
{data.unrecorded > 0 && (
|
i.paths.map((p) => (
|
||||||
<li className="px-1.5 py-1 text-[11px] text-fg-muted">
|
<p
|
||||||
{t("installsUnrecorded", { count: data.unrecorded })}
|
key={p}
|
||||||
</li>
|
className="truncate font-mono text-[10px] text-fg-muted"
|
||||||
)}
|
title={p}
|
||||||
{data.items.length === 0 && data.unrecorded === 0 && (
|
>
|
||||||
<li className="px-1.5 py-1 text-[11px] text-fg-muted">
|
{p}
|
||||||
{t("installsNoneOfYours")}
|
</p>
|
||||||
</li>
|
))
|
||||||
)}
|
) : (
|
||||||
</ul>
|
<p className="font-mono text-[10px] text-fg-muted">
|
||||||
)}
|
{t("installsPluginWhere")}
|
||||||
</div>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{data.unrecorded > 0 && (
|
||||||
|
<li className="px-1.5 py-1 text-[11px] text-fg-muted">
|
||||||
|
{t("installsUnrecorded", { count: data.unrecorded })}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
{data.items.length === 0 && data.unrecorded === 0 && (
|
||||||
|
<li className="px-1.5 py-1 text-[11px] text-fg-muted">
|
||||||
|
{t("installsNoneOfYours")}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,6 +362,11 @@ describe("Artifacts", () => {
|
|||||||
expect(await within(box).findByText("/h/.claude/skills/deploy")).toBeInTheDocument();
|
expect(await within(box).findByText("/h/.claude/skills/deploy")).toBeInTheDocument();
|
||||||
expect(within(box).getByText(/khoa/)).toBeInTheDocument();
|
expect(within(box).getByText(/khoa/)).toBeInTheDocument();
|
||||||
expect(within(box).getByText(/1 earlier installs/)).toBeInTheDocument();
|
expect(within(box).getByText(/1 earlier installs/)).toBeInTheDocument();
|
||||||
|
// Over the page, outside the card list that clips its overflow — and a
|
||||||
|
// click inside it keeps it open.
|
||||||
|
expect(box.parentElement).toBe(document.body);
|
||||||
|
await user.click(within(box).getByText(/khoa/));
|
||||||
|
expect(screen.getByTestId("artifact-installs")).toBeInTheDocument();
|
||||||
await user.keyboard("{Escape}");
|
await user.keyboard("{Escape}");
|
||||||
expect(screen.queryByTestId("artifact-installs")).toBeNull();
|
expect(screen.queryByTestId("artifact-installs")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7316,7 +7316,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
|||||||
<span
|
<span
|
||||||
class="text-status-warning/90"
|
class="text-status-warning/90"
|
||||||
>
|
>
|
||||||
UI build v2.8.36 (rebuild client to match)
|
UI build v2.8.38 (rebuild client to match)
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.36",
|
"version": "2.8.38",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.36",
|
"version": "2.8.38",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "2.8.36",
|
"version": "2.8.38",
|
||||||
"bin": {
|
"bin": {
|
||||||
"agenthub": "bin/agenthub.js"
|
"agenthub": "bin/agenthub.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -555,12 +555,13 @@ async function usage(t0, t1, p0, p1, mem) {
|
|||||||
|
|
||||||
const round = (n) => Math.round(n * 10) / 10;
|
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) => ({
|
const userExtra = (u) => ({
|
||||||
kind: "user",
|
kind: "user",
|
||||||
userId: u.id,
|
userId: u.id,
|
||||||
username: u.username,
|
username: u.username,
|
||||||
displayName: u.displayName ?? null,
|
displayName: u.displayName ?? null,
|
||||||
|
avatarAt: u.avatarAt ?? null,
|
||||||
});
|
});
|
||||||
|
|
||||||
async function collect() {
|
async function collect() {
|
||||||
|
|||||||
Reference in New Issue
Block a user