03f1673fce
- Workspace, SettingsHub, SessionDetail and the Workflow sub-tab are lazy (main bundle 2,166 kB -> ~920 kB), wrapped in RouteErrorBoundary: a stale chunk after a self-update reloads once (vite:preloadError too), any other error shows a Reload panel instead of a blank page. - Live refresh: lib/throttle.ts + hooks/useLiveRefresh.ts replace trailing debounces that froze under a steady event stream; refresh skips hidden tabs and refetches after a WebSocket reconnect. Monitor loads only while shown, with one batched GET /api/agents?session_ids= instead of N+1; a failing endpoint is an inline error, not a blank Dashboard. - NeedsYouStrip on Monitor and above Analytics. Tabs, filters, page, sort and search live in the URL (Dashboard, Sessions, Activity, SessionDetail). - Sessions: tool / workspace / date-range filters, CSV/JSON export (formula-neutralised), focusable row links, Open in workspace (the dead /run links are gone), stale responses dropped, error + Retry. - Fixes: "NaNs" durations, 30/7-day charts skipping idle days, weekday labels west of UTC (now averages), cache hit rate denominator, billable tokens vs cache reads, exact tool-call counts, "To" date = end of day, invalid ?from= no longer throws, recent list no longer shrinks to 15, stable message keys, transcript switch races. - Light-theme-unreadable bg-black code panels and dark-only DAG fills use tokens; status configs and chart colours use theme tokens; Health tab and other literals translated. - Server (additive): GET /api/agents?session_ids=, GET /api/sessions awaiting=1&from=&to=, stats tool_call_count/distinct_tool_count, event rows carry session/agent labels.
172 lines
8.5 KiB
TypeScript
172 lines
8.5 KiB
TypeScript
/**
|
|
* @file StatusBadge.tsx
|
|
* @description Defines reusable React components for displaying the status of agents and sessions in a visually distinct way using badges. The AgentStatusBadge component shows the current status of an agent with an optional pulsing effect for active states, while the SessionStatusBadge component indicates the status of a session. When a row is in the yellow "Waiting" overlay state, both badges can additionally render WHY it waits (the server's awaiting_reason: needs input / turn done / at prompt / interrupted) as a nested icon+label chip with a hover tooltip carrying the full explanation — or, in `compact` mode for tight card layouts, as the hover tooltip alone. Both components utilize predefined configurations for consistent styling across the application.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
/* =============================================================================
|
|
* MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed)
|
|
* =============================================================================
|
|
* **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode.
|
|
*
|
|
* ## Design constraints
|
|
* - Local-first: no telemetry leaves the machine unless the user configures webhooks.
|
|
* - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
|
|
* philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
|
|
* - Destructive flows stay behind explicit confirmation modals and server-side gates.
|
|
* - Internationalization: user-visible strings belong in i18n JSON, not literals here.
|
|
*
|
|
* ## Remote data & SSH
|
|
* Remote Data Sources let operators aggregate multiple machines. SSH entries describe
|
|
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every
|
|
* scoped GET via `?sources=`. Health checks and import history surface in Settings.
|
|
*
|
|
* ## Internal dependencies
|
|
* - `../lib/types`
|
|
* - `./Tip`
|
|
*
|
|
* ## Public surface
|
|
* - `REASON_ICONS` — exported API; see TSDoc on the symbol for behavior.
|
|
* - `AgentStatusBadge` — exported API; see TSDoc on the symbol for behavior.
|
|
* - `SessionStatusBadge` — exported API; see TSDoc on the symbol for behavior.
|
|
*
|
|
* ## Testing pointers
|
|
* - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
|
|
* - Server contract changes require `npm run test:server` and OpenAPI sync.
|
|
*
|
|
* ## Related docs
|
|
* - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline.
|
|
* - `docs/API.md` — REST reference.
|
|
* - `skills/file-headers/` — mandatory `@author` header policy.
|
|
* ============================================================================= */
|
|
/* -----------------------------------------------------------------------------
|
|
* EXPORT CATALOG — quick index of symbols defined below (documentation only).
|
|
* -----------------------------------------------------------------------------
|
|
* **REASON_ICONS**
|
|
* Part of this module's public contract. Downstream imports should treat
|
|
* the signature and return type as stable unless release notes say otherwise.
|
|
* When behavior changes, update the `@file` overview and relevant tests.
|
|
*
|
|
* **AgentStatusBadge**
|
|
* Part of this module's public contract. Downstream imports should treat
|
|
* the signature and return type as stable unless release notes say otherwise.
|
|
* When behavior changes, update the `@file` overview and relevant tests.
|
|
*
|
|
* **SessionStatusBadge**
|
|
* Part of this module's public contract. Downstream imports should treat
|
|
* the signature and return type as stable unless release notes say otherwise.
|
|
* When behavior changes, update the `@file` overview and relevant tests.
|
|
*
|
|
* ----------------------------------------------------------------------------- */
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
import { BellRing, MessageSquareReply, Terminal, OctagonPause } from "lucide-react";
|
|
import type { LucideIcon } from "lucide-react";
|
|
import { STATUS_CONFIG, SESSION_STATUS_CONFIG, AWAITING_REASON_CONFIG } from "../lib/types";
|
|
import type { EffectiveAgentStatus, EffectiveSessionStatus, AwaitingReason } from "../lib/types";
|
|
import { Tip } from "./Tip";
|
|
|
|
/** Per-reason icon, kept here (not in types.ts) so the presentation lookup in
|
|
* lib/ stays JSX-free. Matches the semantics documented on {@link AwaitingReason}.
|
|
* Exported so richer surfaces (e.g. SessionDetail's waiting banner) reuse the
|
|
* same icon per reason as the badges. */
|
|
export const REASON_ICONS: Record<AwaitingReason, LucideIcon> = {
|
|
notification: BellRing, // blocked on a permission/input prompt - ring the bell
|
|
stop: MessageSquareReply, // Claude replied; your reply is the next move
|
|
session_start: Terminal, // fresh CLI sitting at an empty prompt
|
|
interrupted: OctagonPause, // turn cut short (Esc / recovered hook)
|
|
};
|
|
|
|
/**
|
|
* The "why" chip nested inside a Waiting badge: a small rounded pill with the
|
|
* reason's icon and short label. Urgent reasons (permission prompts,
|
|
* interruptions) get the amber warning fill; the calm idle-between-turns
|
|
* ones stay neutral, so a scan of a list surfaces the rows that actually block on the human.
|
|
*/
|
|
function ReasonChip({ reason }: { reason: AwaitingReason }) {
|
|
const { t } = useTranslation();
|
|
const cfg = AWAITING_REASON_CONFIG[reason];
|
|
const Icon = REASON_ICONS[reason];
|
|
return (
|
|
<span
|
|
className={`inline-flex items-center gap-1 rounded-full border px-1.5 text-[10px] font-medium leading-4 ${
|
|
cfg.urgent
|
|
? "bg-status-warning/15 border-status-warning/25 text-status-warning"
|
|
: "bg-surface-3 border-border text-fg-secondary"
|
|
}`}
|
|
>
|
|
<Icon className="w-2.5 h-2.5 flex-shrink-0" aria-hidden="true" />
|
|
{t(cfg.labelKey)}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
interface AgentStatusBadgeProps {
|
|
status: EffectiveAgentStatus;
|
|
pulse?: boolean;
|
|
/** WHY the agent is waiting (from `agentAwaitingReason`); rendered as a
|
|
* nested icon+label chip with a tooltip. Ignored unless `status` is "waiting". */
|
|
reason?: AwaitingReason | null;
|
|
/** Tooltip-only mode for tight layouts (Kanban/Dashboard cards): keeps the
|
|
* hover explanation but suppresses the inline reason chip so the badge
|
|
* never squeezes the card title. */
|
|
compact?: boolean;
|
|
}
|
|
|
|
export function AgentStatusBadge({ status, pulse, reason, compact }: AgentStatusBadgeProps) {
|
|
const { t } = useTranslation();
|
|
const config = STATUS_CONFIG[status];
|
|
// "waiting" pulses by default so the user's eye is drawn to sessions that
|
|
// need their attention, matching the pulsing for active/working states.
|
|
const shouldPulse = pulse ?? (status === "working" || status === "waiting");
|
|
// Only decorate the Waiting overlay - a reason on any other status is stale.
|
|
const shownReason = status === "waiting" && reason ? reason : null;
|
|
|
|
return (
|
|
// Tip renders children unwrapped when raw is undefined (non-waiting rows).
|
|
<Tip raw={shownReason ? t(AWAITING_REASON_CONFIG[shownReason].descKey) : undefined}>
|
|
<span className={`badge ${config.bg} ${config.color}`}>
|
|
<span
|
|
className={`w-1.5 h-1.5 rounded-full ${config.dot} ${
|
|
shouldPulse ? "animate-pulse-dot" : ""
|
|
}`}
|
|
/>
|
|
{t(config.labelKey)}
|
|
{shownReason && !compact && <ReasonChip reason={shownReason} />}
|
|
</span>
|
|
</Tip>
|
|
);
|
|
}
|
|
|
|
interface SessionStatusBadgeProps {
|
|
status: EffectiveSessionStatus;
|
|
pulse?: boolean;
|
|
/** WHY the session is waiting (from `sessionAwaitingReason`); rendered as a
|
|
* nested icon+label chip with a tooltip. Ignored unless `status` is "waiting". */
|
|
reason?: AwaitingReason | null;
|
|
/** Tooltip-only mode for tight layouts (Kanban/Dashboard cards): keeps the
|
|
* hover explanation but suppresses the inline reason chip so the badge
|
|
* never squeezes the card title. */
|
|
compact?: boolean;
|
|
}
|
|
|
|
export function SessionStatusBadge({ status, pulse, reason, compact }: SessionStatusBadgeProps) {
|
|
const { t } = useTranslation();
|
|
const config = SESSION_STATUS_CONFIG[status];
|
|
const shouldPulse = pulse ?? status === "waiting";
|
|
const shownReason = status === "waiting" && reason ? reason : null;
|
|
return (
|
|
<Tip raw={shownReason ? t(AWAITING_REASON_CONFIG[shownReason].descKey) : undefined}>
|
|
<span className={`badge ${config.bg} ${config.color}`}>
|
|
{shouldPulse && (
|
|
<span
|
|
className={`w-1.5 h-1.5 rounded-full ${config.dot} animate-pulse-dot`}
|
|
aria-hidden="true"
|
|
/>
|
|
)}
|
|
{t(config.labelKey)}
|
|
{shownReason && !compact && <ReasonChip reason={shownReason} />}
|
|
</span>
|
|
</Tip>
|
|
);
|
|
}
|