mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-26 20:45:37 +02:00
* feat(server): broadcast task:running event The dispatched → running transition was silent: only task:queued, task:dispatch, task:cancelled, task:completed and task:failed broadcast over WS. Any UI that distinguishes "queued" from "running" (e.g. the new issue-card agent activity indicator) would lag by up to the 30s agentTaskSnapshot staleTime on the most user-visible transition. StartTask now broadcasts task:running so the workspace snapshot invalidates immediately, keeping the agent activity UI live. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(issues): live agent activity chip + per-issue indicator + filter Surfaces "which agents are working on what, right now" in the Issues and My Issues views, with a one-click filter to narrow the list to issues that have a running agent task. Two visual surfaces: - **Workspace chip** in the header (left of Filter). Shows the brand-tinted avatar stack of agents currently running on visible issues. Click toggles a page-scoped filter; idle state renders a static "0 working" button with a hover-card placeholder. When the filter is active the chip pins to brand fill across hover and popover states (the Button outline variant otherwise repaints back to neutral). A muted "Viewing only working agents" hint sits to the left of the chip whenever the filter is on, so users notice the active state without having to hover. - **Per-issue indicator** on every board card and list row (top-right of the identifier line). Renders the avatar stack of agents in running or queued state on that issue, full-opacity ring at brand/70 when ≥1 is running, half-opacity stack when only queued. Returns null when nothing is in flight. Both surfaces open the same hover-card body that lists each active task with the agent avatar, status dot (composed via the existing availability + workload tokens), and a live-ticking duration. Adds a new "All" scope to /my-issues that unions assignee, creator, and involves_user_id via three parallel fetches deduped on the client — no backend changes for this part. The chip's count and the quick-filter both use the page's currently visible issue ids so they stay in sync with the active scope. State is per-user (Zustand + localStorage) and the agentRunningFilter is intentionally omitted from partialize — running state changes second-to-second and a stored toggle would land users in an unexplained empty list. WS task:running, already added in the preceding commit, drives real-time updates without polling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(issues): swap indicator ring pulse for shimmer text label Earlier iterations layered a brand ring with various opacity-pulse cadences around the per-issue avatar stack. Every tuning attempt was either invisible (transparent ring + faded pulse) or oppressive (a visible ring that flashed on a dense board). Moves the "alive" signal onto a small text label and reuses chat's existing `animate-chat-text-shimmer` utility — a soft light sweep across the glyphs that already powers the ChatGPT-style "thinking" cue in task-status-pill. Indicator now reads as a 12 px avatar stack + 10 px label: - Running → full-opacity avatars + shimmering localized "Working" - Queued → half-opacity avatars + muted static "Queued" - Idle → render nothing (unchanged) Avatars and the surrounding card stay completely still; only the few glyphs animate. The label is i18n-driven via the existing `status_running` / `status_queued` keys, so no locale changes are required. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
93 lines
3.1 KiB
TypeScript
93 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import { ActorAvatar as ActorAvatarBase } from "@multica/ui/components/common/actor-avatar";
|
|
import { useActorName } from "@multica/core/workspace/hooks";
|
|
import { cn } from "@multica/ui/lib/utils";
|
|
|
|
interface AgentAvatarStackProps {
|
|
// Agent ids to render, in display order. The component does NOT dedupe —
|
|
// callers are expected to pass a unique list (`new Set(...)` upstream).
|
|
agentIds: readonly string[];
|
|
// Diameter in px. Avatars overlap by ~30% so the visible spacing scales
|
|
// naturally with size. Defaults match a compact toolbar / card-corner
|
|
// density (18 px).
|
|
size?: number;
|
|
// Maximum head count before collapsing the tail into a `+N` chip. Three
|
|
// is the plan default — beyond that the stack visually crowds.
|
|
max?: number;
|
|
// `half` drops opacity to 50%. Used by IssueAgentActivityIndicator to
|
|
// signal a queued-only state (no running task) — same heads, weakened
|
|
// visual.
|
|
opacity?: "full" | "half";
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* Overlapping avatar group for agents. Pure presentational — no data
|
|
* fetching, no hover handling. Wrap it in a HoverCardTrigger upstream
|
|
* (IssueAgentActivityIndicator / WorkspaceAgentWorkingChip) to surface
|
|
* per-agent detail.
|
|
*
|
|
* `agentIds` is the full input list. We render up to `max` heads; if the
|
|
* input is longer, we drop the tail and append a `+N` overflow chip styled
|
|
* to match the avatar dimensions.
|
|
*/
|
|
export function AgentAvatarStack({
|
|
agentIds,
|
|
size = 18,
|
|
max = 3,
|
|
opacity = "full",
|
|
className,
|
|
}: AgentAvatarStackProps) {
|
|
const { getActorName, getActorInitials, getActorAvatarUrl } = useActorName();
|
|
if (agentIds.length === 0) return null;
|
|
|
|
const visible = agentIds.slice(0, max);
|
|
const overflow = agentIds.length - visible.length;
|
|
// 30% overlap reads as "stacked" without obscuring the next avatar's icon.
|
|
const overlap = Math.round(size * 0.3);
|
|
|
|
return (
|
|
<span
|
|
className={cn(
|
|
"inline-flex items-center",
|
|
opacity === "half" && "opacity-50",
|
|
className,
|
|
)}
|
|
style={{ paddingLeft: 0 }}
|
|
>
|
|
{visible.map((id, i) => (
|
|
<span
|
|
key={id}
|
|
// Each subsequent head sits negative-margin over the previous so
|
|
// the stack collapses horizontally instead of growing linearly.
|
|
style={{ marginLeft: i === 0 ? 0 : -overlap }}
|
|
className="ring-2 ring-background rounded-full inline-flex"
|
|
>
|
|
<ActorAvatarBase
|
|
name={getActorName("agent", id)}
|
|
initials={getActorInitials("agent", id)}
|
|
avatarUrl={getActorAvatarUrl("agent", id)}
|
|
isAgent
|
|
size={size}
|
|
/>
|
|
</span>
|
|
))}
|
|
{overflow > 0 && (
|
|
<span
|
|
style={{
|
|
marginLeft: -overlap,
|
|
width: size,
|
|
height: size,
|
|
fontSize: Math.max(9, Math.round(size * 0.45)),
|
|
}}
|
|
className="ring-2 ring-background rounded-full bg-muted text-muted-foreground inline-flex items-center justify-center font-medium tabular-nums"
|
|
aria-label={`${overflow} more`}
|
|
>
|
|
+{overflow}
|
|
</span>
|
|
)}
|
|
</span>
|
|
);
|
|
}
|