feat(ui): use agent personas throughout the app
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
f5cd8de1b3
commit
0b7d006dd4
|
|
@ -0,0 +1,11 @@
|
|||
import { defineConfig } from "@playwright/test";
|
||||
export default defineConfig({
|
||||
testDir: ".", testMatch: "agent-personas.spec.ts", workers: 1, retries: 0, timeout: 45_000,
|
||||
outputDir: "./test-results/agent-personas", reporter: [["list"]],
|
||||
snapshotPathTemplate: "{testDir}/.snapshots/agent-personas/{arg}{ext}",
|
||||
use: {
|
||||
browserName: "chromium", baseURL: process.env.PAPERCLIP_PERSONA_STORYBOOK_URL ?? "http://127.0.0.1:6017",
|
||||
viewport: { width: 1200, height: 900 }, deviceScaleFactor: 1, reducedMotion: "reduce",
|
||||
launchOptions: { args: ["--use-angle=swiftshader", "--enable-unsafe-swiftshader"] },
|
||||
},
|
||||
});
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import { test, expect, type Page } from "@playwright/test";
|
||||
async function story(page: Page, id: string, args = "", theme = "dark") {
|
||||
await page.goto(`/iframe.html?id=agents-personas--${id}&viewMode=story&globals=theme:${theme}${args ? `&args=${args}` : ""}`);
|
||||
await page.locator("#storybook-root").waitFor();
|
||||
await expect.poll(() => page.locator("#storybook-root").evaluate(element => element.childElementCount)).toBeGreaterThan(0);
|
||||
}
|
||||
async function imagesLoaded(page: Page) {
|
||||
await page.locator("#storybook-root img").evaluateAll(images => images.forEach(image => image.setAttribute("loading", "eager")));
|
||||
await expect.poll(() => page.locator("#storybook-root img").evaluateAll(images => images.every(image => (image as HTMLImageElement).complete && (image as HTMLImageElement).naturalWidth > 0))).toBe(true);
|
||||
}
|
||||
for (const theme of ["light", "dark"]) {
|
||||
for (const id of ["palettes", "sizes", "expressions", "app-placements", "gray-before-connection"]) {
|
||||
test(`${id} on ${theme}`, async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await story(page, id, "", theme); await imagesLoaded(page);
|
||||
expect(await page.evaluate(() => matchMedia("(prefers-reduced-motion: reduce)").matches)).toBe(true);
|
||||
await expect(page.locator("canvas")).toHaveCount(0);
|
||||
await expect(page.locator("#storybook-root")).toHaveScreenshot(`${id}-${theme}.png`, { animations: "disabled", maxDiffPixels: 0 });
|
||||
});
|
||||
}
|
||||
}
|
||||
test("500 avatars load images without WebGL, live modules or avatar frame loops", async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||||
await page.addInitScript(() => {
|
||||
(window as any).__personaProbe = { webgl: 0, frames: 0 };
|
||||
const getContext = HTMLCanvasElement.prototype.getContext;
|
||||
HTMLCanvasElement.prototype.getContext = function (kind: string, ...args: any[]) {
|
||||
if (kind.includes("webgl")) (window as any).__personaProbe.webgl++;
|
||||
return (getContext as any).call(this, kind, ...args);
|
||||
} as any;
|
||||
const raf = window.requestAnimationFrame;
|
||||
window.requestAnimationFrame = callback => {
|
||||
if (/\/(runtime|renderer)-/.test(new Error().stack ?? "")) (window as any).__personaProbe.frames++;
|
||||
return raf(callback);
|
||||
};
|
||||
});
|
||||
const liveDownloads: string[] = [];
|
||||
page.on("request", req => { if (/\/(runtime|renderer)-[^/]+\.js/.test(req.url())) liveDownloads.push(req.url()); });
|
||||
await story(page, "five-hundred-static-avatars"); await imagesLoaded(page);
|
||||
await expect(page.locator("#storybook-root img")).toHaveCount(500);
|
||||
await expect(page.locator("canvas")).toHaveCount(0);
|
||||
expect(await page.evaluate(() => (window as any).__personaProbe)).toEqual({ webgl: 0, frames: 0 });
|
||||
expect(liveDownloads).toEqual([]);
|
||||
});
|
||||
test("image failures and slow cold responses preserve dimensions", async ({ page }) => {
|
||||
let release!: () => void;
|
||||
const held = new Promise<void>(resolve => { release = resolve; });
|
||||
await page.route("**/api/agent-avatars/**", async route => { await held; await route.abort(); });
|
||||
await story(page, "cache-miss-loading");
|
||||
const image = page.locator("#storybook-root img");
|
||||
const before = await image.boundingBox();
|
||||
expect(before?.width).toBe(64); expect(before?.height).toBe(64);
|
||||
release();
|
||||
await expect(page.locator("#storybook-root")).toContainText("CS");
|
||||
const fallback = page.locator("#storybook-root span").filter({ hasText: /^CS$/ }).first();
|
||||
expect((await fallback.boundingBox())?.width).toBe(64);
|
||||
});
|
||||
test("one live renderer and static fallback after context loss", async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||||
await story(page, "one-live-renderer");
|
||||
await expect(page.locator("canvas")).toHaveCount(1);
|
||||
await story(page, "render-failure");
|
||||
await expect(page.locator("canvas")).toHaveCount(1);
|
||||
await page.getByRole("button", { name: "Simulate WebGL loss" }).click();
|
||||
await expect(page.locator("canvas")).toHaveCount(0); await imagesLoaded(page);
|
||||
});
|
||||
test("repeated mounting releases the WebGL canvas", async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||||
await story(page, "mount-and-unmount");
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await expect(page.locator("canvas")).toHaveCount(1);
|
||||
await page.getByRole("button", { name: "Toggle character" }).click();
|
||||
await expect(page.locator("canvas")).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "Toggle character" }).click();
|
||||
}
|
||||
});
|
||||
const snapshotCases = [
|
||||
...[16, 24, 48, 256].flatMap(size => [1, 2].map(density => ({ size, density, state: "rest" }))),
|
||||
...["idle", "listening", "thinking", "working", "success", "confused", "sleepy", "loading"].map(state => ({ size: 128, density: 2, state })),
|
||||
];
|
||||
for (const { size, density, state } of snapshotCases) {
|
||||
test(`front-facing SVG and WebGL ${state} at ${size}px density ${density}`, async ({ browser }) => {
|
||||
const context = await browser.newContext({ deviceScaleFactor: density, reducedMotion: "reduce", baseURL: process.env.PAPERCLIP_PERSONA_STORYBOOK_URL ?? "http://127.0.0.1:6017" });
|
||||
const page = await context.newPage();
|
||||
await story(page, "snapshot-agreement", `size:${size};state:${state};density:${density}`); await imagesLoaded(page);
|
||||
await expect(page.locator("canvas")).toHaveCount(1);
|
||||
await expect(page.locator("#storybook-root")).toHaveScreenshot(`snapshot-pair-${size}-${density}-${state}.png`, { scale: "device", maxDiffPixels: 0 });
|
||||
// Same transparent silhouette. Small antialiasing differences are expected
|
||||
// between sharp's SVG rasterizer and WebGL's multisample rasterizer.
|
||||
const difference = await page.evaluate(async () => {
|
||||
const image = document.querySelector('#storybook-root img') as HTMLImageElement;
|
||||
const live = document.querySelector('#live-frame canvas, [data-testid="live-frame"] canvas') as HTMLCanvasElement;
|
||||
const width = live.width, height = live.height;
|
||||
const a = document.createElement("canvas"); a.width = width; a.height = height;
|
||||
const b = document.createElement("canvas"); b.width = width; b.height = height;
|
||||
const ac = a.getContext("2d")!, bc = b.getContext("2d")!;
|
||||
ac.drawImage(image, 0, 0, width, height); bc.drawImage(live, 0, 0, width, height);
|
||||
const aa = ac.getImageData(0, 0, width, height).data, bb = bc.getImageData(0, 0, width, height).data;
|
||||
let silhouette = 0, colorError = 0, opaque = 0;
|
||||
for (let i = 0; i < aa.length; i += 4) {
|
||||
if ((aa[i + 3] > 128) !== (bb[i + 3] > 128)) silhouette++;
|
||||
if (aa[i + 3] > 240 && bb[i + 3] > 240) { for (let c = 0; c < 3; c++) colorError += Math.abs(aa[i + c] - bb[i + c]); opaque++; }
|
||||
}
|
||||
return { silhouette: silhouette / (width * height), meanColorError: colorError / (opaque * 3) };
|
||||
});
|
||||
expect(difference.silhouette).toBeLessThan(0.04);
|
||||
expect(difference.meanColorError).toBeLessThan(20);
|
||||
await context.close();
|
||||
});
|
||||
}
|
||||
|
||||
const fullPages = ["all-agents", "agent-overview", "task", "company-dashboard", "meet-your-next-agent", "new-agent-connection"];
|
||||
for (const id of fullPages) {
|
||||
test(`full app page: ${id}`, async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await page.clock.setFixedTime(new Date("2026-09-10T18:00:00Z"));
|
||||
await page.goto(`/iframe.html?id=agents-personas-full-pages--${id}&viewMode=story&globals=theme:dark`);
|
||||
await expect(page.locator("main")).toBeVisible();
|
||||
await expect(page.locator("main")).not.toContainText("This page hit an error");
|
||||
await expect(page.locator('img[src*="/api/agent-avatars/"]').first()).toBeAttached();
|
||||
await imagesLoaded(page);
|
||||
await expect(page.locator("canvas")).toHaveCount(0);
|
||||
await expect(page).toHaveScreenshot(`full-page-${id}.png`, { animations: "disabled", maxDiffPixels: 0 });
|
||||
});
|
||||
}
|
||||
for (const density of [1, 2]) {
|
||||
test(`onboarding supersamples and stays inside the canvas at density ${density}`, async ({ browser }) => {
|
||||
const context = await browser.newContext({ viewport: { width: 1200, height: 900 }, deviceScaleFactor: density, reducedMotion: "no-preference", baseURL: process.env.PAPERCLIP_PERSONA_STORYBOOK_URL ?? "http://127.0.0.1:6017" });
|
||||
const page = await context.newPage();
|
||||
await page.goto("/iframe.html?id=agents-personas-full-pages--meet-your-next-agent&viewMode=story");
|
||||
await expect(page.locator("canvas")).toHaveCount(1);
|
||||
const canvas = page.locator("canvas");
|
||||
const dimensions = await canvas.evaluate(c => ({ pixels: (c as HTMLCanvasElement).width, display: c.getBoundingClientRect().width }));
|
||||
expect(dimensions.display).toBe(192);
|
||||
expect(dimensions.pixels).toBe(dimensions.display * density * 2);
|
||||
for (const [x, y] of [[5, 5], [1195, 5], [1195, 895], [5, 895]]) {
|
||||
await page.mouse.move(x, y);
|
||||
// Let gaze settle at each page corner, well outside the character region.
|
||||
await page.waitForTimeout(350);
|
||||
const edgeAlpha = await canvas.evaluate(c => {
|
||||
const source = c as HTMLCanvasElement;
|
||||
const copy = document.createElement("canvas"); copy.width = source.width; copy.height = source.height;
|
||||
const ctx = copy.getContext("2d")!; ctx.drawImage(source, 0, 0);
|
||||
const pixels = ctx.getImageData(0, 0, copy.width, copy.height).data;
|
||||
let maximum = 0;
|
||||
for (let y = 0; y < copy.height; y++) for (let x = 0; x < copy.width; x++) {
|
||||
if (x < 2 || y < 2 || x >= copy.width - 2 || y >= copy.height - 2) maximum = Math.max(maximum, pixels[(y * copy.width + x) * 4 + 3]);
|
||||
}
|
||||
return maximum;
|
||||
});
|
||||
expect(edgeAlpha).toBe(0);
|
||||
}
|
||||
await context.close();
|
||||
});
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import type { AgentAppearance } from "@paperclipai/shared";
|
||||
import type { IssueRecoveryAction } from "@paperclipai/shared";
|
||||
import type {
|
||||
HeartbeatRun,
|
||||
|
|
@ -31,6 +32,8 @@ export interface ActiveRunForIssue {
|
|||
createdAt: string | Date;
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
agentAppearance?: AgentAppearance | null;
|
||||
avatarUrl?: string;
|
||||
adapterType: string;
|
||||
logBytes?: number | null;
|
||||
lastOutputBytes?: number | null;
|
||||
|
|
@ -62,6 +65,8 @@ export interface LiveRunForIssue {
|
|||
createdAt: string;
|
||||
agentId: string;
|
||||
agentName: string;
|
||||
agentAppearance?: AgentAppearance | null;
|
||||
avatarUrl?: string;
|
||||
adapterType: string;
|
||||
logBytes?: number | null;
|
||||
lastOutputBytes?: number | null;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { memo, useMemo } from "react";
|
||||
import { Link } from "@/lib/router";
|
||||
import { useQueries, useQuery } from "@tanstack/react-query";
|
||||
|
|
@ -200,7 +201,7 @@ const AgentRunCard = memo(function AgentRunCard({
|
|||
) : (
|
||||
<span className="inline-flex h-2.5 w-2.5 rounded-full bg-muted-foreground/35" />
|
||||
)}
|
||||
<Identity name={run.agentName} size="sm" className="[&>span:last-child]:!text-(length:--text-micro)" />
|
||||
<AgentIdentity agent={{ id: run.agentId, name: run.agentName, appearance: run.agentAppearance }} size="sm" className="[&>span:last-child]:!text-(length:--text-micro)" />
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2 text-(length:--text-micro) text-muted-foreground">
|
||||
<span>{(run.execution?.phase === "reconnecting" || run.execution?.phase === "retry_scheduled") ? "Reconnecting…" : (isActive ? "Live now" : run.finishedAt ? `Finished ${relativeTime(run.finishedAt)}` : `Started ${relativeTime(run.createdAt)}`)}</span>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useMemo, useState, useRef, useCallback, useEffect } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useVisibilityRefetchInterval } from "@/lib/polling";
|
||||
|
|
@ -26,7 +27,6 @@ import {
|
|||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ListFilter, Layers, ChevronDown, ChevronRight, User, Settings } from "lucide-react";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { timeAgo } from "../lib/timeAgo";
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
|
@ -280,7 +280,7 @@ function CollapsedFeedGroup({
|
|||
: <ChevronRight className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
}
|
||||
{group.latestEvent.actorType === "agent"
|
||||
? <AgentIcon icon={actor?.icon ?? null} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
? <AgentAvatar agent={actor} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
: group.latestEvent.actorType === "user"
|
||||
? <User className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
: <Settings className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "./AgentAvatar";
|
||||
import { Link } from "@/lib/router";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import { deriveInitials } from "./Identity";
|
||||
|
|
@ -55,10 +56,10 @@ export function ActivityRow({ event, agentMap, userProfileMap, entityNameMap, en
|
|||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<Avatar size="xs">
|
||||
{event.actorType === "agent" ? <AgentAvatar agent={actor} name={actorName} size={24} /> : <Avatar size="xs">
|
||||
{actorAvatarUrl && <AvatarImage src={actorAvatarUrl} alt={actorName} />}
|
||||
<AvatarFallback>{deriveInitials(actorName)}</AvatarFallback>
|
||||
</Avatar>
|
||||
</Avatar>}
|
||||
<p className="min-w-0 flex-1 truncate">
|
||||
<span>{actorName}</span>
|
||||
<span className="text-muted-foreground"> {verb} </span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useEffect, useMemo, useState, type ComponentProps, type ReactNode } from "react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -94,7 +94,7 @@ export function AgentSelect({
|
|||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<AgentIcon icon={agent.icon ?? null} className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={agent} size={16} className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground"/>
|
||||
<span className="flex min-w-0 flex-col">
|
||||
<span className="truncate text-sm font-medium text-foreground">{agent.name}</span>
|
||||
{agent.title ? <span className="truncate text-xs text-muted-foreground">{agent.title}</span> : null}
|
||||
|
|
@ -261,7 +261,7 @@ export function AgentMultiSelect({
|
|||
setSelection(next);
|
||||
}}
|
||||
/>
|
||||
<AgentIcon icon={agent.icon ?? null} className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={agent} size={16} className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground"/>
|
||||
<span className="flex min-w-0 flex-col">
|
||||
<span className="flex items-center gap-1.5 text-sm font-medium text-foreground">
|
||||
<span className="truncate">{agent.name}</span>
|
||||
|
|
@ -306,7 +306,7 @@ export function AgentMultiSelect({
|
|||
<div className="space-y-0.5">
|
||||
{selectedAgents.slice(0, 3).map((agent) => (
|
||||
<div key={agent.id} className="flex items-center gap-2 px-1.5 py-1 text-sm">
|
||||
<AgentIcon icon={agent.icon ?? null} className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={agent} size={16} className="h-4 w-4 shrink-0 text-muted-foreground"/>
|
||||
<span className="min-w-0 flex-1 truncate text-foreground">{agent.name}</span>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link } from "@/lib/router";
|
||||
import { AGENT_ROLE_LABELS, type Agent, type AgentRuntimeState } from "@paperclipai/shared";
|
||||
|
|
@ -96,7 +97,7 @@ export function AgentProperties({ agent, runtimeState }: AgentPropertiesProps) {
|
|||
<PropertyRow label="Reports To">
|
||||
{reportsToAgent ? (
|
||||
<Link to={agentUrl(reportsToAgent)} className="hover:underline">
|
||||
<Identity name={reportsToAgent.name} size="sm" />
|
||||
<AgentIdentity agent={reportsToAgent} size="sm" />
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-sm font-mono">{agent.reportsTo.slice(0, 8)}</span>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { CheckCircle2, XCircle, Clock } from "lucide-react";
|
||||
import { Link } from "@/lib/router";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
|
@ -71,7 +72,7 @@ export function ApprovalCard({
|
|||
{requesterAgent && (
|
||||
<div className="inline-flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
|
||||
<span>Requested by</span>
|
||||
<Identity name={requesterAgent.name} size="sm" className="inline-flex" />
|
||||
<AgentIdentity agent={requesterAgent} size="sm" className="inline-flex" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AlertTriangle, CheckCircle2 } from "lucide-react";
|
||||
|
|
@ -29,6 +30,7 @@ interface BlockedInboxViewProps {
|
|||
companyId: string;
|
||||
searchQuery: string;
|
||||
agentNameById: ReadonlyMap<string, string>;
|
||||
agents?: import("./AgentAvatar").AvatarAgent[];
|
||||
userLabelById?: ReadonlyMap<string, string>;
|
||||
issueLinkState: unknown;
|
||||
groupBy: BlockedInboxGroupBy;
|
||||
|
|
@ -50,6 +52,7 @@ export function BlockedInboxView({
|
|||
companyId,
|
||||
searchQuery,
|
||||
agentNameById,
|
||||
agents,
|
||||
userLabelById,
|
||||
issueLinkState,
|
||||
groupBy,
|
||||
|
|
@ -217,7 +220,7 @@ export function BlockedInboxView({
|
|||
key={row.issue.id}
|
||||
row={row}
|
||||
issueLinkState={issueLinkState}
|
||||
agentNameById={agentNameById}
|
||||
agentNameById={agentNameById} agents={agents}
|
||||
userLabelById={userLabelById}
|
||||
liveIssueIds={liveIssueIds}
|
||||
subtreeLiveCounts={subtreeLiveCounts}
|
||||
|
|
@ -247,7 +250,7 @@ export function BlockedInboxView({
|
|||
key={row.issue.id}
|
||||
row={row}
|
||||
issueLinkState={issueLinkState}
|
||||
agentNameById={agentNameById}
|
||||
agentNameById={agentNameById} agents={agents}
|
||||
userLabelById={userLabelById}
|
||||
liveIssueIds={liveIssueIds}
|
||||
subtreeLiveCounts={subtreeLiveCounts}
|
||||
|
|
@ -272,6 +275,7 @@ interface BlockedInboxRowProps {
|
|||
row: BlockedInboxIssueRow;
|
||||
issueLinkState: unknown;
|
||||
agentNameById: ReadonlyMap<string, string>;
|
||||
agents?: import("./AgentAvatar").AvatarAgent[];
|
||||
userLabelById?: ReadonlyMap<string, string>;
|
||||
liveIssueIds: ReadonlySet<string>;
|
||||
subtreeLiveCounts: ReadonlyMap<string, number>;
|
||||
|
|
@ -301,6 +305,7 @@ function BlockedInboxRow({
|
|||
row,
|
||||
issueLinkState,
|
||||
agentNameById,
|
||||
agents,
|
||||
userLabelById,
|
||||
liveIssueIds,
|
||||
subtreeLiveCounts,
|
||||
|
|
@ -330,11 +335,7 @@ function BlockedInboxRow({
|
|||
</span>
|
||||
{ownerName ? (
|
||||
<span className="hidden w-(--sz-150px) min-w-0 items-center text-muted-foreground sm:inline-flex">
|
||||
<Identity
|
||||
name={ownerName}
|
||||
size="xs"
|
||||
className="max-w-full"
|
||||
/>
|
||||
{isAgent ? <AgentIdentity agent={agents?.find((agent) => agent.id === row.attention.owner.agentId) ?? { id: row.attention.owner.agentId ?? undefined, name: ownerName }} size="xs" className="max-w-full" /> : <Identity name={ownerName} size="xs" className="max-w-full" />}
|
||||
</span>
|
||||
) : (
|
||||
<span className="hidden w-(--sz-150px) shrink-0 sm:inline-flex" aria-hidden="true" />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useLocation, useNavigate } from "@/lib/router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
|
@ -408,7 +409,7 @@ export function CommandPalette() {
|
|||
<span className="flex-1 truncate">{issue.title}</span>
|
||||
{issue.assigneeAgentId && (() => {
|
||||
const name = agentName(issue.assigneeAgentId);
|
||||
return name ? <Identity name={name} size="sm" className="ml-2 hidden sm:inline-flex" /> : null;
|
||||
return name ? <AgentIdentity agent={agents?.find((agent) => agent.id === issue.assigneeAgentId) ?? { id: issue.assigneeAgentId, name }} size="sm" className="ml-2 hidden sm:inline-flex" /> : null;
|
||||
})()}
|
||||
</CommandItem>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { AgentIdentity } from "./AgentIdentity";
|
||||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { memo, useEffect, useMemo, useRef, useState, type ChangeEvent } from "react";
|
||||
import { Link, useLocation } from "react-router-dom";
|
||||
import type {
|
||||
|
|
@ -18,7 +20,6 @@ import { MarkdownBody, type MarkdownExternalReferenceMap } from "./MarkdownBody"
|
|||
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor";
|
||||
import { OutputFeedbackButtons } from "./OutputFeedbackButtons";
|
||||
import { ApprovalCard } from "./ApprovalCard";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { formatAssigneeUserLabel } from "../lib/assignees";
|
||||
import { formatTimelineWorkspaceLabel, type IssueTimelineAssignee, type IssueTimelineEvent } from "../lib/issue-timeline-events";
|
||||
import { timeAgo } from "../lib/timeAgo";
|
||||
|
|
@ -344,8 +345,8 @@ function CommentCard({
|
|||
<div className="flex items-center justify-between mb-1">
|
||||
{comment.authorAgentId ? (
|
||||
<Link to={`/agents/${comment.authorAgentId}`} className="hover:underline">
|
||||
<Identity
|
||||
name={agentMap?.get(comment.authorAgentId)?.name ?? comment.authorAgentId.slice(0, 8)}
|
||||
<AgentIdentity
|
||||
agent={agentMap?.get(comment.authorAgentId) ?? { id: comment.authorAgentId, name: comment.authorAgentId.slice(0, 8) }}
|
||||
size="sm"
|
||||
/>
|
||||
</Link>
|
||||
|
|
@ -478,9 +479,9 @@ function TimelineEventCard({
|
|||
|
||||
return (
|
||||
<div id={`activity-${event.id}`} className="flex items-start gap-2.5 py-1.5">
|
||||
<Avatar size="sm" className="mt-0.5">
|
||||
<AvatarFallback>{initialsForName(actorName)}</AvatarFallback>
|
||||
</Avatar>
|
||||
{event.actorType === "agent" ? <AgentAvatar agent={agentMap?.get(event.actorId) ?? { id: event.actorId, name: actorName }} size={32} className="mt-0.5" /> : (
|
||||
<Avatar size="sm" className="mt-0.5"><AvatarFallback>{initialsForName(actorName)}</AvatarFallback></Avatar>
|
||||
)}
|
||||
|
||||
<div className="min-w-0 flex-1 space-y-1.5">
|
||||
<div className="flex flex-wrap items-baseline gap-x-1.5 gap-y-1 text-sm">
|
||||
|
|
@ -624,9 +625,7 @@ const TimelineList = memo(function TimelineList({
|
|||
const actorName = agentMap?.get(run.agentId)?.name ?? run.agentId.slice(0, 8);
|
||||
return (
|
||||
<div id={`run-${run.runId}`} key={`run:${run.runId}`} className="flex items-center gap-2.5 py-1.5">
|
||||
<Avatar size="sm">
|
||||
<AvatarFallback>{initialsForName(actorName)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<AgentAvatar agent={agentMap?.get(run.agentId) ?? { id: run.agentId, name: actorName }} size={32} />
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm">
|
||||
|
|
@ -829,6 +828,7 @@ export function CommentThread({
|
|||
kind: "agent",
|
||||
agentId: a.id,
|
||||
agentIcon: a.icon,
|
||||
agentAppearance: a.appearance,
|
||||
}));
|
||||
}, [agentMap, providedMentions]);
|
||||
|
||||
|
|
@ -1060,7 +1060,7 @@ export function CommentThread({
|
|||
return (
|
||||
<>
|
||||
{agent ? (
|
||||
<AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
) : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
@ -1073,7 +1073,7 @@ export function CommentThread({
|
|||
return (
|
||||
<>
|
||||
{agent ? (
|
||||
<AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
) : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "./AgentAvatar";
|
||||
import { useMemo, useRef, useState, useEffect } from "react";
|
||||
import type {
|
||||
DocumentAnnotationComment,
|
||||
|
|
@ -23,7 +24,6 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|||
import { cn, relativeTime } from "@/lib/utils";
|
||||
import type { DocumentAnnotationTarget } from "@/api/document-annotations";
|
||||
import { copyTextToClipboard } from "@/lib/clipboard";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { deriveInitials } from "./Identity";
|
||||
import { MarkdownBody } from "./MarkdownBody";
|
||||
import type { PendingAnchor } from "./DocumentAnnotationLayer";
|
||||
|
|
@ -59,7 +59,7 @@ export interface AnnotationPanelProps {
|
|||
inline?: boolean;
|
||||
className?: string;
|
||||
/** Resolve `<authorAgentId>` to a display name. */
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon" | "appearance">>>;
|
||||
/** Resolve `<authorUserId>` to a display name. */
|
||||
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
|
||||
}
|
||||
|
|
@ -326,7 +326,7 @@ export function ThreadCard(props: {
|
|||
onCopyLink: () => void;
|
||||
pendingReply: boolean;
|
||||
pendingStatus: boolean;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon" | "appearance">>>;
|
||||
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
|
||||
}) {
|
||||
const { thread } = props;
|
||||
|
|
@ -461,7 +461,7 @@ function CommentRow({
|
|||
}: {
|
||||
comment: DocumentAnnotationComment;
|
||||
focused: boolean;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon" | "appearance">>>;
|
||||
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
|
||||
}) {
|
||||
const author = resolveAuthor(comment, { agentMap, userProfileMap });
|
||||
|
|
@ -476,18 +476,11 @@ function CommentRow({
|
|||
>
|
||||
<div className="mb-0.5 flex items-center justify-between gap-2 text-(length:--text-micro)">
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
{author.role === "agent" ? <AgentAvatar agent={author.agent} size={20} /> : (
|
||||
<Avatar size="xs" className="shrink-0">
|
||||
{author.role === "agent" ? (
|
||||
<AvatarFallback>
|
||||
<AgentIcon icon={author.agentIcon} className="h-3 w-3" />
|
||||
</AvatarFallback>
|
||||
) : (
|
||||
<>
|
||||
{author.imageUrl ? <AvatarImage src={author.imageUrl} alt={author.name} /> : null}
|
||||
<AvatarFallback>{deriveInitials(author.name)}</AvatarFallback>
|
||||
</>
|
||||
)}
|
||||
</Avatar>
|
||||
{author.imageUrl ? <AvatarImage src={author.imageUrl} alt={author.name} /> : null}
|
||||
<AvatarFallback>{deriveInitials(author.name)}</AvatarFallback>
|
||||
</Avatar>)}
|
||||
<span className="truncate font-medium text-foreground">{author.name}</span>
|
||||
{author.role === "agent" ? (
|
||||
<span className="text-muted-foreground">· agent</span>
|
||||
|
|
@ -508,16 +501,17 @@ function isSubmitShortcut(event: React.KeyboardEvent<HTMLTextAreaElement>): bool
|
|||
function resolveAuthor(
|
||||
comment: DocumentAnnotationComment,
|
||||
maps: {
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon" | "appearance">>>;
|
||||
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
|
||||
},
|
||||
): { name: string; role: "board" | "agent"; agentIcon?: Agent["icon"]; imageUrl?: string | null } {
|
||||
): { name: string; role: "board" | "agent"; agentIcon?: Agent["icon"]; agent?: import("./AgentAvatar").AvatarAgent; imageUrl?: string | null } {
|
||||
if (comment.authorAgentId) {
|
||||
const agent = maps.agentMap?.get(comment.authorAgentId);
|
||||
return {
|
||||
name: agent?.name ?? comment.authorAgentId.slice(0, 8),
|
||||
role: "agent",
|
||||
agentIcon: agent?.icon,
|
||||
agent: agent ?? { id: comment.authorAgentId },
|
||||
};
|
||||
}
|
||||
if (comment.authorUserId) {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar, type AvatarAgent } from "./AgentAvatar";
|
||||
import type { ReactNode } from "react";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { cn, relativeTime } from "../lib/utils";
|
||||
|
|
@ -13,13 +14,13 @@ import {
|
|||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { deriveInitials } from "./Identity";
|
||||
|
||||
export type DocumentFrameHeaderRevisionActor = {
|
||||
kind: "agent" | "user" | "system";
|
||||
name: string;
|
||||
agentIcon?: string | null;
|
||||
agent?: AvatarAgent;
|
||||
imageUrl?: string | null;
|
||||
};
|
||||
|
||||
|
|
@ -57,18 +58,11 @@ export interface DocumentFrameHeaderProps {
|
|||
}
|
||||
|
||||
function RevisionActorAvatar({ actor }: { actor: DocumentFrameHeaderRevisionActor }) {
|
||||
if (actor.kind === "agent") return <AgentAvatar agent={actor.agent} name={actor.name} size={20} />;
|
||||
return (
|
||||
<Avatar size="xs" shape={actor.kind === "agent" ? "square" : "circle"} className="shrink-0">
|
||||
{actor.kind === "agent" ? (
|
||||
<AvatarFallback>
|
||||
<AgentIcon icon={actor.agentIcon} className="h-3 w-3" />
|
||||
</AvatarFallback>
|
||||
) : (
|
||||
<>
|
||||
{actor.imageUrl ? <AvatarImage src={actor.imageUrl} alt={actor.name} /> : null}
|
||||
<AvatarFallback>{deriveInitials(actor.name)}</AvatarFallback>
|
||||
</>
|
||||
)}
|
||||
<Avatar size="xs" className="shrink-0">
|
||||
{actor.imageUrl ? <AvatarImage src={actor.imageUrl} alt={actor.name} /> : null}
|
||||
<AvatarFallback>{deriveInitials(actor.name)}</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useMemo, useState } from "react";
|
||||
import type { Agent, Issue } from "@paperclipai/shared";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
|
@ -13,7 +14,6 @@ import {
|
|||
import { cn } from "../lib/utils";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { User, Eye, ShieldCheck } from "lucide-react";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
|
||||
type StageType = "review" | "approval";
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ export function ExecutionParticipantPicker({
|
|||
)}
|
||||
onClick={() => toggle(encoded)}
|
||||
>
|
||||
<AgentIcon icon={agent.icon} className="shrink-0 h-3 w-3 text-muted-foreground" />
|
||||
<AgentAvatar agent={agent} size={16} className="shrink-0 h-3 w-3 text-muted-foreground"/>
|
||||
{agent.name}
|
||||
</button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { Link } from "@/lib/router";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { timeAgo } from "../lib/timeAgo";
|
||||
import { cn } from "../lib/utils";
|
||||
import { deriveProjectUrlKey, type ActivityEvent, type Agent } from "@paperclipai/shared";
|
||||
|
|
@ -380,10 +380,8 @@ function resolveContent(
|
|||
function ActorGlyph({ content }: { content: CardContent }) {
|
||||
if (content.actorType === "agent") {
|
||||
return (
|
||||
<AgentIcon
|
||||
icon={content.actor?.icon ?? null}
|
||||
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
<AgentAvatar agent={content.actor} size={16}
|
||||
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
);
|
||||
}
|
||||
if (content.actorType === "user") {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { AssistantRuntimeProvider } from "@assistant-ui/react";
|
||||
import type {
|
||||
ReasoningMessagePart,
|
||||
|
|
@ -145,7 +146,6 @@ import {
|
|||
type InlineEntityOption,
|
||||
} from "./InlineEntitySelector";
|
||||
import { IssueThreadInteractionCard } from "./IssueThreadInteractionCard";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import {
|
||||
AssigneeChip,
|
||||
ComposerHandoffPreviewRow,
|
||||
|
|
@ -1287,8 +1287,8 @@ function IssueChatChainOfThought({
|
|||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<span className="inline-flex items-center gap-2 text-sm font-medium text-foreground/80">
|
||||
{agentIcon ? (
|
||||
<AgentIcon icon={agentIcon} className="h-4 w-4 shrink-0" />
|
||||
{agentId ? (
|
||||
<AgentAvatar agent={agentId ? agentMap?.get(agentId) ?? { id: agentId } : undefined} size={16} />
|
||||
) : isActive ? (
|
||||
<Loader2 className="h-4 w-4 shrink-0 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
|
|
@ -2417,17 +2417,7 @@ function IssueChatAssistantMessage({
|
|||
!isRunning &&
|
||||
(hasCommentText || deleted);
|
||||
|
||||
const agentAvatar = (
|
||||
<Avatar size="sm" className="shrink-0">
|
||||
{agentIcon ? (
|
||||
<AvatarFallback>
|
||||
<AgentIcon icon={agentIcon} className="h-3.5 w-3.5" />
|
||||
</AvatarFallback>
|
||||
) : (
|
||||
<AvatarFallback>{initialsForName(authorName)}</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
);
|
||||
const agentAvatar = <AgentAvatar agent={agentId ? agentMap?.get(agentId) ?? { id: agentId, name: authorName } : { name: authorName }} size={32} />;
|
||||
|
||||
const messageActionBar = (
|
||||
<div className="mt-2 flex items-center gap-1">
|
||||
|
|
@ -2553,8 +2543,8 @@ function IssueChatAssistantMessage({
|
|||
{/* Icon + name together in a header ABOVE the bubble (PAP-95 rev 7). */}
|
||||
<div className="mb-1 flex items-center gap-1.5 px-1">
|
||||
<span className="flex size-5 shrink-0 items-center justify-center text-muted-foreground">
|
||||
{agentIcon ? (
|
||||
<AgentIcon icon={agentIcon} className="h-4 w-4" />
|
||||
{agentId ? (
|
||||
<AgentAvatar agent={agentId ? agentMap?.get(agentId) ?? { id: agentId } : undefined} size={16} />
|
||||
) : (
|
||||
<Avatar size="sm" className="size-5">
|
||||
<AvatarFallback className="text-(length:--text-nano)">
|
||||
|
|
@ -2706,11 +2696,8 @@ function IssueChatAssistantMessage({
|
|||
<div className="rounded-lg px-1 py-2">
|
||||
<div className="flex min-w-0 items-center gap-2.5">
|
||||
<span className="inline-flex items-center gap-2 text-sm font-medium text-foreground/80">
|
||||
{agentIcon ? (
|
||||
<AgentIcon
|
||||
icon={agentIcon}
|
||||
className="h-4 w-4 shrink-0"
|
||||
/>
|
||||
{agentId ? (
|
||||
<AgentAvatar agent={agentId ? agentMap?.get(agentId) ?? { id: agentId } : undefined} size={16} />
|
||||
) : (
|
||||
<Loader2 className="h-4 w-4 shrink-0 animate-spin text-muted-foreground" />
|
||||
)}
|
||||
|
|
@ -3080,15 +3067,11 @@ function ExpiredRequestConfirmationActivity({
|
|||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-2.5 py-1">
|
||||
<Avatar size="sm" className="mt-0.5">
|
||||
{actorIcon ? (
|
||||
<AvatarFallback>
|
||||
<AgentIcon icon={actorIcon} className="h-3.5 w-3.5" />
|
||||
</AvatarFallback>
|
||||
) : (
|
||||
<AvatarFallback>{initialsForName(actorName)}</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
{actorAgentId ? (
|
||||
<AgentAvatar agent={agentMap?.get(actorAgentId) ?? { id: actorAgentId, name: actorName }} size={32} />
|
||||
) : (
|
||||
<Avatar size="sm" className="mt-0.5"><AvatarFallback>{initialsForName(actorName)}</AvatarFallback></Avatar>
|
||||
)}
|
||||
{rowContent}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -3800,15 +3783,11 @@ function IssueChatSystemMessage({ message }: { message: ThreadMessage }) {
|
|||
|
||||
if (custom.kind === "event" && actorName) {
|
||||
const isAgent = actorType === "agent";
|
||||
const agentIcon =
|
||||
isAgent && actorId ? agentMap?.get(actorId)?.icon : undefined;
|
||||
const isCurrentUser =
|
||||
actorType === "user" && !!currentUserId && actorId === currentUserId;
|
||||
const rowIcon = agentIcon ? (
|
||||
<AgentIcon icon={agentIcon} className="h-3 w-3" />
|
||||
) : (
|
||||
<ClipboardList className="h-3 w-3" />
|
||||
);
|
||||
const agentIcon = isAgent && actorId ? agentMap?.get(actorId)?.icon : undefined;
|
||||
const isCurrentUser = actorType === "user" && !!currentUserId && actorId === currentUserId;
|
||||
const rowIcon = isAgent
|
||||
? <AgentAvatar agent={actorId ? agentMap?.get(actorId) ?? { id: actorId } : undefined} size={16} />
|
||||
: <ClipboardList className="h-3 w-3" />;
|
||||
const handoffResolvers: HandoffChipResolvers = {
|
||||
agentMap,
|
||||
currentUserId,
|
||||
|
|
@ -3903,18 +3882,8 @@ function IssueChatSystemMessage({ message }: { message: ThreadMessage }) {
|
|||
? (agentMap?.get(runAgentId)?.name ?? runAgentId.slice(0, 8))
|
||||
: null);
|
||||
const runAgentIcon = runAgentId ? agentMap?.get(runAgentId)?.icon : undefined;
|
||||
if (
|
||||
custom.kind === "run" &&
|
||||
runId &&
|
||||
runAgentId &&
|
||||
displayedRunAgentName &&
|
||||
runStatus
|
||||
) {
|
||||
const rowIcon = runAgentIcon ? (
|
||||
<AgentIcon icon={runAgentIcon} className="h-3 w-3" />
|
||||
) : (
|
||||
<span className="h-1.5 w-1.5 rounded-full bg-muted-foreground/50" />
|
||||
);
|
||||
if (custom.kind === "run" && runId && runAgentId && displayedRunAgentName && runStatus) {
|
||||
const rowIcon = <AgentAvatar agent={agentMap?.get(runAgentId) ?? { id: runAgentId }} size={16} />;
|
||||
|
||||
return (
|
||||
<IssueChatMetadataRow anchorId={anchorId} icon={rowIcon}>
|
||||
|
|
@ -5585,10 +5554,7 @@ const IssueChatComposer = forwardRef<
|
|||
return (
|
||||
<>
|
||||
{agent ? (
|
||||
<AgentIcon
|
||||
icon={agent.icon}
|
||||
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
<AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
) : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
@ -5604,10 +5570,7 @@ const IssueChatComposer = forwardRef<
|
|||
return (
|
||||
<>
|
||||
{agent ? (
|
||||
<AgentIcon
|
||||
icon={agent.icon}
|
||||
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
<AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
) : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ describe("InboxIssueMetaLeading live state", () => {
|
|||
});
|
||||
|
||||
describe("InboxIssueTrailingColumns attribution", () => {
|
||||
it("renders a kicked off by column for agent creators with square identity", () => {
|
||||
it("renders a kicked off by column for agent creators with a character identity", () => {
|
||||
const text = renderLeading(
|
||||
<InboxIssueTrailingColumns
|
||||
issue={makeIssue({
|
||||
|
|
@ -155,7 +155,7 @@ describe("InboxIssueTrailingColumns attribution", () => {
|
|||
);
|
||||
|
||||
expect(text).toContain("CodexCoder");
|
||||
expect(container?.querySelector('[data-shape="square"]')).not.toBeNull();
|
||||
expect(container?.querySelector('[data-slot="agent-avatar"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
it("renders a kicked off by column for user creators", () => {
|
||||
|
|
@ -208,7 +208,7 @@ describe("InboxIssueTrailingColumns attribution", () => {
|
|||
// The responsible user wins over the creating agent.
|
||||
expect(text).toContain("Morgan Product");
|
||||
expect(container?.querySelector('[data-shape="circle"]')).not.toBeNull();
|
||||
expect(container?.querySelector('[data-shape="square"]')).toBeNull();
|
||||
expect(container?.querySelector('[data-slot="agent-avatar"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("surfaces the responsible user for a routine execution with no creator", () => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import type { AvatarAgent } from "./AgentAvatar";
|
||||
import type { ReactNode } from "react";
|
||||
import { deriveOriginatingActor, type Issue } from "@paperclipai/shared";
|
||||
import { Columns3 } from "lucide-react";
|
||||
|
|
@ -271,6 +273,8 @@ export function InboxIssueTrailingColumns({
|
|||
workspaceId,
|
||||
workspaceName,
|
||||
assigneeName,
|
||||
assigneeAgent,
|
||||
creatorAgent,
|
||||
assigneeUserName,
|
||||
assigneeUserAvatarUrl,
|
||||
creatorAgentName,
|
||||
|
|
@ -290,6 +294,8 @@ export function InboxIssueTrailingColumns({
|
|||
workspaceId?: string | null;
|
||||
workspaceName: string | null;
|
||||
assigneeName: string | null;
|
||||
assigneeAgent?: AvatarAgent;
|
||||
creatorAgent?: AvatarAgent;
|
||||
assigneeUserName?: string | null;
|
||||
assigneeUserAvatarUrl?: string | null;
|
||||
creatorAgentName?: string | null;
|
||||
|
|
@ -322,10 +328,9 @@ export function InboxIssueTrailingColumns({
|
|||
if (issue.assigneeAgentId) {
|
||||
return (
|
||||
<span key={column} className="min-w-0 text-xs text-foreground">
|
||||
<Identity
|
||||
name={assigneeName ?? issue.assigneeAgentId.slice(0, 8)}
|
||||
<AgentIdentity
|
||||
agent={assigneeAgent ?? { id: issue.assigneeAgentId, name: assigneeName ?? issue.assigneeAgentId.slice(0, 8) }}
|
||||
size="sm"
|
||||
shape="square"
|
||||
className="min-w-0"
|
||||
/>
|
||||
</span>
|
||||
|
|
@ -359,10 +364,9 @@ export function InboxIssueTrailingColumns({
|
|||
<Tooltip key={column}>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="min-w-0 text-xs text-foreground">
|
||||
<Identity
|
||||
name={name}
|
||||
<AgentIdentity
|
||||
agent={creatorAgent ?? { id: originatingActor.id, name }}
|
||||
size="sm"
|
||||
shape="square"
|
||||
className="min-w-0"
|
||||
/>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -648,7 +648,7 @@ describe("IssueDocumentAnnotations", () => {
|
|||
expect(expandedText).toContain("UXDesigner");
|
||||
expect(expandedText).toContain("· agent");
|
||||
// Each rendered comment shows an author avatar.
|
||||
const avatars = expandedThread?.querySelectorAll('[data-slot="avatar"]') ?? [];
|
||||
const avatars = expandedThread?.querySelectorAll('[data-slot="avatar"], [data-slot="agent-avatar"]') ?? [];
|
||||
expect(avatars.length).toBe(2);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -589,7 +589,7 @@ describe("IssueDocumentsSection", () => {
|
|||
expect(document.body.textContent).toContain("CodexCoder");
|
||||
expect(document.body.textContent).toContain("Dotta");
|
||||
expect(document.body.textContent).not.toContain("• agent");
|
||||
expect(document.body.querySelectorAll('[data-slot="avatar"]').length).toBeGreaterThanOrEqual(2);
|
||||
expect(document.body.querySelectorAll('[data-slot="avatar"], [data-slot="agent-avatar"]').length).toBeGreaterThanOrEqual(2);
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ function downloadDocumentFile(key: string, body: string) {
|
|||
function getRevisionActor(
|
||||
revision: DocumentRevision,
|
||||
maps: {
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon" | "appearance">>>;
|
||||
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
|
||||
},
|
||||
): DocumentFrameHeaderRevisionActor {
|
||||
|
|
@ -159,6 +159,7 @@ function getRevisionActor(
|
|||
kind: "agent",
|
||||
name: agent?.name ?? revision.createdByAgentId.slice(0, 8),
|
||||
agentIcon: agent?.icon ?? null,
|
||||
agent: agent ?? { id: revision.createdByAgentId },
|
||||
};
|
||||
}
|
||||
if (revision.createdByUserId) {
|
||||
|
|
@ -275,7 +276,7 @@ export function IssueDocumentsSection({
|
|||
options?: { allowSharing?: boolean; reason?: string },
|
||||
) => Promise<void>;
|
||||
extraActions?: ReactNode;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon" | "appearance">>>;
|
||||
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
|
||||
/**
|
||||
* Seed which document annotation panels are open on first render. Mostly useful
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { startTransition, useDeferredValue, useEffect, useMemo, useState, useCallback, useRef } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -2310,6 +2311,8 @@ function StreamlinedIssuesList({
|
|||
})}
|
||||
onFilterWorkspace={filterToWorkspace}
|
||||
assigneeName={agentName(issue.assigneeAgentId)}
|
||||
assigneeAgent={agents?.find((agent) => agent.id === issue.assigneeAgentId)}
|
||||
creatorAgent={agents?.find((agent) => agent.id === issue.createdByAgentId)}
|
||||
assigneeUserName={assigneeUserLabel}
|
||||
assigneeUserAvatarUrl={assigneeUserProfile?.image ?? null}
|
||||
creatorAgentName={agentName(issue.createdByAgentId)}
|
||||
|
|
@ -2333,7 +2336,7 @@ function StreamlinedIssuesList({
|
|||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
>
|
||||
{issue.assigneeAgentId && agentName(issue.assigneeAgentId) ? (
|
||||
<Identity name={agentName(issue.assigneeAgentId)!} size="sm" shape="square" className="min-w-0" />
|
||||
<AgentIdentity agent={agents!.find((agent) => agent.id === issue.assigneeAgentId)!} size="sm" className="min-w-0" />
|
||||
) : issue.assigneeUserId ? (
|
||||
<Identity
|
||||
name={assigneeUserLabel ?? "User"}
|
||||
|
|
@ -2412,7 +2415,7 @@ function StreamlinedIssuesList({
|
|||
assignIssue(issue.id, agent.id, null);
|
||||
}}
|
||||
>
|
||||
<Identity name={agent.name} size="sm" className="min-w-0" />
|
||||
<AgentIdentity agent={agent} size="sm" className="min-w-0" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "@/lib/router";
|
||||
import {
|
||||
|
|
@ -367,7 +368,7 @@ function KanbanCard({
|
|||
{issue.assigneeAgentId && (() => {
|
||||
const name = agentName(issue.assigneeAgentId);
|
||||
return name ? (
|
||||
<Identity name={name} size="xs" />
|
||||
<AgentIdentity agent={agents?.find((agent) => agent.id === issue.assigneeAgentId) ?? { id: issue.assigneeAgentId, name }} size="xs" />
|
||||
) : (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{issue.assigneeAgentId.slice(0, 8)}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { startTransition, useDeferredValue, useEffect, useMemo, useState, useCallback, useRef } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -2203,6 +2204,8 @@ export function IssuesList({
|
|||
})}
|
||||
onFilterWorkspace={filterToWorkspace}
|
||||
assigneeName={agentName(issue.assigneeAgentId)}
|
||||
assigneeAgent={agents?.find((agent) => agent.id === issue.assigneeAgentId)}
|
||||
creatorAgent={agents?.find((agent) => agent.id === issue.createdByAgentId)}
|
||||
assigneeUserName={assigneeUserLabel}
|
||||
assigneeUserAvatarUrl={assigneeUserProfile?.image ?? null}
|
||||
creatorAgentName={agentName(issue.createdByAgentId)}
|
||||
|
|
@ -2226,7 +2229,7 @@ export function IssuesList({
|
|||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}
|
||||
>
|
||||
{issue.assigneeAgentId && agentName(issue.assigneeAgentId) ? (
|
||||
<Identity name={agentName(issue.assigneeAgentId)!} size="sm" shape="square" className="min-w-0" />
|
||||
<AgentIdentity agent={agents!.find((agent) => agent.id === issue.assigneeAgentId)!} size="sm" className="min-w-0" />
|
||||
) : issue.assigneeUserId ? (
|
||||
<Identity
|
||||
name={assigneeUserLabel ?? "User"}
|
||||
|
|
@ -2305,7 +2308,7 @@ export function IssuesList({
|
|||
assignIssue(issue.id, agent.id, null);
|
||||
}}
|
||||
>
|
||||
<Identity name={agent.name} size="sm" className="min-w-0" />
|
||||
<AgentIdentity agent={agent} size="sm" className="min-w-0" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link } from "@/lib/router";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -113,7 +114,7 @@ export function LiveRunWidget({ issueId, companyId }: LiveRunWidgetProps) {
|
|||
<div className="mb-3 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div className="min-w-0">
|
||||
<Link to={`/agents/${run.agentId}`} className="inline-flex hover:underline">
|
||||
<Identity name={run.agentName} size="sm" />
|
||||
<AgentIdentity agent={{ id: run.agentId, name: run.agentName, appearance: run.agentAppearance }} size="sm" />
|
||||
</Link>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||
<Link
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "./AgentAvatar";
|
||||
import {
|
||||
Component,
|
||||
type ClipboardEvent,
|
||||
|
|
@ -45,7 +46,6 @@ import {
|
|||
buildUserMentionHref,
|
||||
} from "@paperclipai/shared";
|
||||
import { Boxes, CalendarClock, Flag, Hash, User, X } from "lucide-react";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { applyMentionChipDecoration, clearMentionChipDecoration, parseMentionChipHref } from "../lib/mention-chips";
|
||||
import { MentionAwareLinkNode, mentionAwareLinkNodeReplacement } from "../lib/mention-aware-link-node";
|
||||
import { mentionDeletionPlugin } from "../lib/mention-deletion";
|
||||
|
|
@ -68,6 +68,7 @@ export interface MentionOption {
|
|||
kind?: "agent" | "project" | "user" | "issue";
|
||||
agentId?: string;
|
||||
agentIcon?: string | null;
|
||||
agentAppearance?: import("@paperclipai/shared").AgentAppearance | null;
|
||||
projectId?: string;
|
||||
projectColor?: string | null;
|
||||
userId?: string;
|
||||
|
|
@ -1596,10 +1597,7 @@ export const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>
|
|||
) : option.kind === "user" ? (
|
||||
<User className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
) : (
|
||||
<AgentIcon
|
||||
icon={option.agentIcon}
|
||||
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
||||
/>
|
||||
<AgentAvatar agent={{ id: option.agentId ?? option.id, name: option.name, appearance: option.agentAppearance }} size={16} />
|
||||
)}
|
||||
{option.kind === "issue" && option.issueIdentifier ? (
|
||||
<span className="flex min-w-0 items-baseline gap-1.5">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { normalizeLegacyRunnerProvider } from "@paperclipai/adapter-utils";
|
||||
import { memo, useState, useEffect, useRef, useCallback, useMemo, type ChangeEvent, type CSSProperties, type DragEvent, type RefObject } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -77,7 +78,6 @@ import { extractProviderIdWithFallback } from "../lib/model-utils";
|
|||
import { issueStatusText, issueStatusTextDefault, priorityColor, priorityColorDefault } from "../lib/status-colors";
|
||||
import { SHOW_TASK_PRIORITY_UI } from "../lib/ui-flags";
|
||||
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { InlineBanner } from "./InlineBanner";
|
||||
import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector";
|
||||
import { getTrustPreset } from "../lib/trust-policy-ui";
|
||||
|
|
@ -1507,7 +1507,7 @@ export function NewIssueDialog() {
|
|||
option ? (
|
||||
currentAssignee ? (
|
||||
<>
|
||||
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={currentAssignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -1524,7 +1524,7 @@ export function NewIssueDialog() {
|
|||
: null;
|
||||
return (
|
||||
<>
|
||||
{assignee ? <AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{assignee ? <AgentAvatar agent={assignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
{assignee && getTrustPreset(assignee.permissions) === "low_trust_review" ? (
|
||||
<ShieldAlert className="ml-auto h-3.5 w-3.5 shrink-0 text-amber-600 dark:text-amber-300" aria-label="Low-trust review agent" />
|
||||
|
|
@ -1665,7 +1665,7 @@ export function NewIssueDialog() {
|
|||
const reviewer = parseAssigneeValue(option.id).assigneeAgentId
|
||||
? (agents ?? []).find((a) => a.id === parseAssigneeValue(option.id).assigneeAgentId)
|
||||
: null;
|
||||
return reviewer ? <AgentIcon icon={reviewer.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null;
|
||||
return reviewer ? <AgentAvatar agent={reviewer} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null;
|
||||
})()}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
@ -1680,7 +1680,7 @@ export function NewIssueDialog() {
|
|||
: null;
|
||||
return (
|
||||
<>
|
||||
{reviewer ? <AgentIcon icon={reviewer.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{reviewer ? <AgentAvatar agent={reviewer} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
@ -1710,7 +1710,7 @@ export function NewIssueDialog() {
|
|||
const approver = parseAssigneeValue(option.id).assigneeAgentId
|
||||
? (agents ?? []).find((a) => a.id === parseAssigneeValue(option.id).assigneeAgentId)
|
||||
: null;
|
||||
return approver ? <AgentIcon icon={approver.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null;
|
||||
return approver ? <AgentAvatar agent={approver} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null;
|
||||
})()}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
@ -1725,7 +1725,7 @@ export function NewIssueDialog() {
|
|||
: null;
|
||||
return (
|
||||
<>
|
||||
{approver ? <AgentIcon icon={approver.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{approver ? <AgentAvatar agent={approver} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
@ -1747,7 +1747,7 @@ export function NewIssueDialog() {
|
|||
>
|
||||
{selectedWatchdogAgent ? (
|
||||
<>
|
||||
<AgentIcon icon={selectedWatchdogAgent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={selectedWatchdogAgent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
<span className="truncate text-foreground">{selectedWatchdogAgent.name}</span>
|
||||
{watchdogInstructions.trim() ? (
|
||||
<span className="truncate text-muted-foreground">· {watchdogInstructions.trim()}</span>
|
||||
|
|
@ -1773,7 +1773,7 @@ export function NewIssueDialog() {
|
|||
option ? (
|
||||
<>
|
||||
{selectedWatchdogAgent ? (
|
||||
<AgentIcon icon={selectedWatchdogAgent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={selectedWatchdogAgent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
) : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
@ -1785,7 +1785,7 @@ export function NewIssueDialog() {
|
|||
const agent = (agents ?? []).find((a) => a.id === option.id);
|
||||
return (
|
||||
<>
|
||||
{agent ? <AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{agent ? <AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { storeProviderApiKey } from "../lib/provider-credential";
|
||||
import { SavedProviderKeySelect, useSavedProviderKeys } from "./onboarding/SavedProviderKeySelect";
|
||||
import { randomAgentAppearance, resolveAgentAppearance, agentAppearanceSchema } from "@paperclipai/shared";
|
||||
import { AgentCharacter } from "./AgentCharacter";
|
||||
import { useEffect, useState, useMemo, useRef } from "react";
|
||||
import type { ComponentType, CSSProperties } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -550,6 +552,7 @@ function OnboardingWizardInner({
|
|||
// on the customer's behalf that they then have to notice and undo. It is the
|
||||
// step's only question, and its CTA gates on it.
|
||||
const [agentName, setAgentName] = useState((saved?.agentName as string) ?? "");
|
||||
const [agentAppearance, setAgentAppearance] = useState(() => agentAppearanceSchema.safeParse(saved?.agentAppearance).data ?? randomAgentAppearance());
|
||||
// Defaults to `general` rather than empty. The arc stopped asking for a role
|
||||
// — a customer naming their first agent is describing what it does, not
|
||||
// filing it — but the hire still needs one, and the guard below returns
|
||||
|
|
@ -764,6 +767,7 @@ function OnboardingWizardInner({
|
|||
* hand rather than the one before it.
|
||||
*/
|
||||
function clearCompanyScopedState() {
|
||||
setAgentAppearance(randomAgentAppearance());
|
||||
setCreatedCompanyPrefix(null);
|
||||
setCompanyName("");
|
||||
setCreatedCompanyGoalId(null);
|
||||
|
|
@ -867,7 +871,7 @@ function OnboardingWizardInner({
|
|||
if (!effectiveOnboardingOpen) return;
|
||||
const state = {
|
||||
step, companyName,
|
||||
agentName, agentRole, adapterType, cwd, model, command, args, url,
|
||||
agentName, agentAppearance, agentRole, adapterType, cwd, model, command, args, url,
|
||||
// The mode, never the key: this blob is localStorage.
|
||||
credentialMode, credentialModeChoice,
|
||||
createdCompanyId, createdCompanyPrefix, createdAgentId,
|
||||
|
|
@ -876,7 +880,7 @@ function OnboardingWizardInner({
|
|||
onboardingDraftStorage.write(JSON.stringify(state));
|
||||
}, [
|
||||
effectiveOnboardingOpen, step, companyName,
|
||||
agentName, agentRole, adapterType, cwd, model, command, args, url,
|
||||
agentName, agentAppearance, agentRole, adapterType, cwd, model, command, args, url,
|
||||
credentialMode, credentialModeChoice,
|
||||
createdCompanyId, createdCompanyPrefix, createdAgentId,
|
||||
createdCompanyGoalId, createdProjectId, createdIssueRef,
|
||||
|
|
@ -1515,6 +1519,7 @@ function OnboardingWizardInner({
|
|||
// Back to the mount defaults: an empty name (the step's only question, and
|
||||
// what its CTA gates on) and the neutral role every onboarding hire uses.
|
||||
setAgentName("");
|
||||
setAgentAppearance(randomAgentAppearance());
|
||||
setAgentRole(DEFAULT_AGENT_ROLE);
|
||||
setAdapterType("claude_local");
|
||||
setModel("");
|
||||
|
|
@ -2050,6 +2055,7 @@ function OnboardingWizardInner({
|
|||
if (existing) {
|
||||
if (!stillTheSameCompany(createdCompanyId)) return;
|
||||
setCreatedAgentId(existing.id);
|
||||
setAgentAppearance(resolveAgentAppearance(existing.appearance, existing.id));
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.agents.list(createdCompanyId)
|
||||
});
|
||||
|
|
@ -2061,6 +2067,7 @@ function OnboardingWizardInner({
|
|||
// The name is optional; an agent that reaches here without one is
|
||||
// named for the job it was hired to do rather than left blank.
|
||||
name: hireName,
|
||||
appearance: agentAppearance,
|
||||
role: agentRole,
|
||||
adapterType,
|
||||
adapterConfig: hireAdapterConfig,
|
||||
|
|
@ -2335,7 +2342,7 @@ function OnboardingWizardInner({
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* The hero, above the heading: one PillGuy held in the same tree
|
||||
{/* The hero, above the heading: one character held in the same tree
|
||||
slot across steps 3–5, so React reuses the DOM node and moving
|
||||
between steps never replays the entrance. It is dormant while
|
||||
the agent is being specified and wakes on Review. */}
|
||||
|
|
@ -2368,14 +2375,7 @@ function OnboardingWizardInner({
|
|||
to this box and travel out past its top-right
|
||||
corner. */}
|
||||
<div className="relative size-(--sz-72px)">
|
||||
<PillGuy
|
||||
state={step === 5 ? "alive" : "dormant"}
|
||||
className="size-full"
|
||||
/>
|
||||
{/* Only while it is actually asleep. A still grey
|
||||
silhouette reads as a placeholder that failed to
|
||||
load rather than as something waiting its turn. */}
|
||||
{step < 5 && <SleepingZs />}
|
||||
<AgentCharacter appearance={agentAppearance} size={128} state={step === 5 ? "success" : adapterEnvLoading || loading || ["loading", "waiting", "connecting"].includes(connectPhase) ? "loading" : "sleepy"} muted={step < 5} className="size-full" />
|
||||
</div>
|
||||
<AgentPreview agentName={agentName} agentRole="" />
|
||||
</motion.div>
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ type CaseBodyDocument = PipelineCaseDocumentPayload["document"] & {
|
|||
function getPipelineRevisionActor(
|
||||
revision: { createdByAgentId?: string | null; createdByUserId?: string | null },
|
||||
maps: {
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon" | "appearance">>>;
|
||||
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
|
||||
},
|
||||
): DocumentFrameHeaderRevisionActor {
|
||||
|
|
@ -48,6 +48,7 @@ function getPipelineRevisionActor(
|
|||
kind: "agent",
|
||||
name: agent?.name ?? revision.createdByAgentId.slice(0, 8),
|
||||
agentIcon: agent?.icon ?? null,
|
||||
agent: agent ?? { id: revision.createdByAgentId },
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -76,7 +77,7 @@ export interface PipelineItemBodyDocumentProps {
|
|||
/** Active conversation issue the body document is/should be anchored to. */
|
||||
conversationIssueId: string | null;
|
||||
conversationIssue: Issue | null;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon">>>;
|
||||
agentMap?: ReadonlyMap<string, Pick<Agent, "id" | "name"> & Partial<Pick<Agent, "icon" | "appearance">>>;
|
||||
userProfileMap?: ReadonlyMap<string, CompanyUserProfile>;
|
||||
mentions?: MentionOption[];
|
||||
imageUploadHandler?: (file: File) => Promise<string>;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useState } from "react";
|
||||
import type { Agent } from "@paperclipai/shared";
|
||||
import {
|
||||
|
|
@ -8,7 +9,6 @@ import {
|
|||
import { User } from "lucide-react";
|
||||
import { cn } from "../lib/utils";
|
||||
import { roleLabels } from "./agent-config-primitives";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
|
||||
export function ReportsToPicker({
|
||||
agents,
|
||||
|
|
@ -55,7 +55,7 @@ export function ReportsToPicker({
|
|||
</>
|
||||
) : current ? (
|
||||
<>
|
||||
<AgentIcon icon={current.icon} className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={current} size={16} className="h-3 w-3 shrink-0 text-muted-foreground"/>
|
||||
<span
|
||||
className={cn(
|
||||
"min-w-0 truncate",
|
||||
|
|
@ -91,7 +91,7 @@ export function ReportsToPicker({
|
|||
</button>
|
||||
{terminatedManager && (
|
||||
<div className="flex min-w-0 items-center gap-2 overflow-hidden px-2 py-1.5 text-xs text-muted-foreground border-b border-border mb-0.5">
|
||||
<AgentIcon icon={current.icon} className="shrink-0 h-3 w-3" />
|
||||
<AgentAvatar agent={current} size={16} className="shrink-0 h-3 w-3"/>
|
||||
<span className="min-w-0 truncate">
|
||||
Current: {current.name} (terminated)
|
||||
</span>
|
||||
|
|
@ -115,7 +115,7 @@ export function ReportsToPicker({
|
|||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<AgentIcon icon={a.icon} className="shrink-0 h-3 w-3 text-muted-foreground" />
|
||||
<AgentAvatar agent={a} size={16} className="shrink-0 h-3 w-3 text-muted-foreground"/>
|
||||
<span className="min-w-0 truncate">{a.name}</span>
|
||||
<span className="text-muted-foreground ml-auto shrink-0">{roleLabels[a.role] ?? a.role}</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import type { ReactNode } from "react";
|
||||
import { MoreHorizontal, Play } from "lucide-react";
|
||||
import { Link } from "@/lib/router";
|
||||
import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -148,7 +148,7 @@ export function RoutineListRow<TRoutine extends RoutineListRowItem>({
|
|||
<span>{routine.projectId ? (project?.name ?? "Unknown project") : "No project"}</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-2">
|
||||
{agent?.icon ? <AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0" /> : null}
|
||||
{agent?.icon ? <AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0"/> : null}
|
||||
<span>{routine.assigneeAgentId ? (agent?.name ?? "Unknown agent") : "No default agent"}</span>
|
||||
</span>
|
||||
<span>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
WORKSPACE_BRANCH_ROUTINE_VARIABLE,
|
||||
|
|
@ -12,7 +13,6 @@ import { useQuery } from "@tanstack/react-query";
|
|||
import { instanceSettingsApi } from "../api/instanceSettings";
|
||||
import { queryKeys } from "../lib/queryKeys";
|
||||
import { IssueWorkspaceCard } from "./IssueWorkspaceCard";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { InlineEntitySelector, type InlineEntityOption } from "./InlineEntitySelector";
|
||||
import { getRecentAssigneeIds, sortAgentsByRecency, trackRecentAssignee } from "../lib/recent-assignees";
|
||||
import { getRecentProjectIds, trackRecentProject } from "../lib/recent-projects";
|
||||
|
|
@ -374,7 +374,7 @@ export function RoutineRunVariablesDialog({
|
|||
option ? (
|
||||
currentAssignee ? (
|
||||
<>
|
||||
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={currentAssignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -389,7 +389,7 @@ export function RoutineRunVariablesDialog({
|
|||
const assignee = agents.find((agent) => agent.id === option.id);
|
||||
return (
|
||||
<>
|
||||
{assignee ? <AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{assignee ? <AgentAvatar agent={assignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, useLocation } from "@/lib/router";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -43,7 +44,6 @@ import {
|
|||
type AgentSidebarSortMode,
|
||||
writeAgentSortMode,
|
||||
} from "../lib/agent-order";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { BudgetSidebarMarker } from "./BudgetSidebarMarker";
|
||||
import { SidebarNavItem } from "./SidebarNavItem.production";
|
||||
import { SidebarSection, type SidebarSectionRadioChoice } from "./SidebarSection";
|
||||
|
|
@ -166,7 +166,7 @@ function SidebarAgentItem({
|
|||
<SidebarNavItem
|
||||
to={href}
|
||||
label={agent.name}
|
||||
iconNode={<AgentIcon icon={agent.icon} className="shrink-0 h-4 w-4" />}
|
||||
iconNode={<AgentAvatar agent={agent} size={16} className="shrink-0 h-4 w-4"/>}
|
||||
active={isActive}
|
||||
liveCount={runCount}
|
||||
labelClassName={showBuiltInLifecycle ? "min-w-(--sz-4_5rem) flex-initial" : undefined}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, useLocation } from "@/lib/router";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -43,7 +44,6 @@ import {
|
|||
type AgentSidebarSortMode,
|
||||
writeAgentSortMode,
|
||||
} from "../lib/agent-order";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { BudgetSidebarMarker } from "./BudgetSidebarMarker";
|
||||
import { SidebarNavItem } from "./SidebarNavItem";
|
||||
import { SidebarSection, type SidebarSectionRadioChoice } from "./SidebarSection";
|
||||
|
|
@ -166,7 +166,7 @@ function SidebarAgentItem({
|
|||
<SidebarNavItem
|
||||
to={href}
|
||||
label={agent.name}
|
||||
iconNode={<AgentIcon icon={agent.icon} className="shrink-0 h-4 w-4" />}
|
||||
iconNode={<AgentAvatar agent={agent} size={16} className="shrink-0 h-4 w-4"/>}
|
||||
active={isActive}
|
||||
liveCount={runCount}
|
||||
labelClassName={showBuiltInLifecycle ? "min-w-(--sz-4_5rem) flex-initial" : undefined}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { CompanySecret, RoutineEnvConfig } from "@paperclipai/shared";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { EmptyState } from "./EmptyState";
|
||||
import { EnvironmentVariablesEditor } from "./environment-variables-editor";
|
||||
import { AgentIcon } from "./AgentIconPicker";
|
||||
import { AgentAvatar, type AvatarAgent } from "./AgentAvatar";
|
||||
|
||||
export interface StageSecretsPanelProps {
|
||||
/** Whether the stage has a backing automation routine with an assignee. */
|
||||
|
|
@ -11,6 +11,7 @@ export interface StageSecretsPanelProps {
|
|||
/** Display name + icon of the agent that runs this step (when automation exists). */
|
||||
agentName?: string | null;
|
||||
agentIcon?: string | null;
|
||||
agent?: AvatarAgent;
|
||||
/** Company secret inventory (shared, not stage-scoped). */
|
||||
secrets: CompanySecret[];
|
||||
secretsLoading: boolean;
|
||||
|
|
@ -35,6 +36,7 @@ export function StageSecretsPanel({
|
|||
hasAutomation,
|
||||
agentName,
|
||||
agentIcon,
|
||||
agent,
|
||||
secrets,
|
||||
secretsLoading,
|
||||
value,
|
||||
|
|
@ -65,7 +67,7 @@ export function StageSecretsPanel({
|
|||
<div className="space-y-5">
|
||||
<div className="flex items-start gap-2 rounded-md border border-border bg-muted/20 px-4 py-3 text-xs text-muted-foreground">
|
||||
{agentName ? (
|
||||
<AgentIcon icon={agentIcon} className="h-3.5 w-3.5 mt-0.5 shrink-0" />
|
||||
<AgentAvatar agent={agent} name={displayName} size={16} />
|
||||
) : (
|
||||
<KeyRound className="h-3.5 w-3.5 mt-0.5 shrink-0" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1556,6 +1556,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
|
|||
agentName:
|
||||
meta?.agentName ??
|
||||
(meta?.agentId ? agentMap?.get(meta.agentId)?.name : undefined),
|
||||
agent: meta?.agentId ? agentMap?.get(meta.agentId) ?? { id: meta.agentId } : undefined,
|
||||
agentIcon: meta?.agentId
|
||||
? agentMap?.get(meta.agentId)?.icon
|
||||
: undefined,
|
||||
|
|
@ -1764,7 +1765,8 @@ export function TaskChatThread(props: TaskChatThreadProps) {
|
|||
agentName:
|
||||
meta?.agentName ??
|
||||
(meta?.agentId ? agentMap?.get(meta.agentId)?.name : undefined),
|
||||
agentIcon: meta?.agentId
|
||||
agent: meta?.agentId ? agentMap?.get(meta.agentId) ?? { id: meta.agentId } : undefined,
|
||||
agentIcon: meta?.agentId
|
||||
? agentMap?.get(meta.agentId)?.icon
|
||||
: undefined,
|
||||
standaloneHeader: sourceIsPaperclipRunner,
|
||||
|
|
@ -1854,6 +1856,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
|
|||
(liveRun.agentId
|
||||
? agentMap?.get(liveRun.agentId)?.name
|
||||
: undefined),
|
||||
agent: liveRun.agentId ? agentMap?.get(liveRun.agentId) ?? { id: liveRun.agentId } : undefined,
|
||||
agentIcon: liveRun.agentId
|
||||
? agentMap?.get(liveRun.agentId)?.icon
|
||||
: undefined,
|
||||
|
|
@ -2767,6 +2770,7 @@ export function TaskChatThread(props: TaskChatThreadProps) {
|
|||
}
|
||||
agentName={visibleTailAgentName}
|
||||
agentIcon={visibleTailAgentIcon}
|
||||
agent={tailAgent ?? (tailAgentId ? { id: tailAgentId } : undefined)}
|
||||
items={tailItems}
|
||||
status={
|
||||
optimisticRunnerStartup
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { AlertTriangle, Info, PauseCircle, User, X } from "lucide-react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { AgentIcon } from "../AgentIconPicker";
|
||||
import { AgentAvatar, type AvatarAgent } from "../AgentAvatar";
|
||||
import {
|
||||
classifyAssigneeHandoff,
|
||||
resolveRunStatusPresentation,
|
||||
|
|
@ -17,7 +17,7 @@ import {
|
|||
* so they can be exercised in isolation by component tests and Storybook.
|
||||
*/
|
||||
|
||||
export interface HandoffAgentLike {
|
||||
export interface HandoffAgentLike extends AvatarAgent {
|
||||
name: string;
|
||||
icon?: string | null;
|
||||
}
|
||||
|
|
@ -60,7 +60,7 @@ export function AssigneeChip({
|
|||
return (
|
||||
<span className={cn(CHIP_CLASS, className)} data-testid="handoff-assignee-chip" data-kind="agent">
|
||||
<span className="sr-only">Agent </span>
|
||||
<AgentIcon icon={agentIcon(assignee.agentId, resolvers)} className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={{ ...resolvers.agentMap?.get(assignee.agentId), id: assignee.agentId }} size={16} />
|
||||
<span className="max-w-(--sz-12rem) truncate">{agentName(assignee.agentId, resolvers)}</span>
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { normalizeLegacyRunnerProvider } from "@paperclipai/adapter-utils";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState, type ComponentType } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
|
@ -78,7 +80,6 @@ import {
|
|||
import { IssuePropertiesPlansTab } from "./IssuePropertiesPlansTab";
|
||||
import { IssuePropertiesArtifactsTab } from "./IssuePropertiesArtifactsTab";
|
||||
import { User, ArrowUpRight, Plus, X, GitBranch, FolderOpen, HardDrive, Check, Clock, RotateCcw, Loader2, CheckCircle2, ArchiveRestore, ChevronLeft } from "lucide-react";
|
||||
import { AgentIcon } from "../AgentIconPicker";
|
||||
import { InlineEntitySelector, type InlineEntityOption } from "../InlineEntitySelector";
|
||||
import {
|
||||
AssigneeRunningBanner,
|
||||
|
|
@ -949,7 +950,7 @@ export function IssueProperties({
|
|||
// --- Interrupt-handoff clarity for the assignee picker (design surface 2) ---
|
||||
const handoffResolvers: HandoffChipResolvers = useMemo(
|
||||
() => ({
|
||||
agentMap: new Map((agents ?? []).map((agent) => [agent.id, { name: agent.name, icon: agent.icon }])),
|
||||
agentMap: new Map((agents ?? []).map((agent) => [agent.id, agent])),
|
||||
resolveUserLabel: (id) => userLabel(id),
|
||||
}),
|
||||
// userLabel closes over userLabelMap + currentUserId, both reflected here.
|
||||
|
|
@ -1141,7 +1142,7 @@ export function IssueProperties({
|
|||
<span className="inline-flex min-w-0 max-w-full items-center gap-1.5 text-sm" title={issue.watchdog.instructions?.trim() || undefined}>
|
||||
{(() => {
|
||||
const agent = (agents ?? []).find((candidate) => candidate.id === issue.watchdog?.watchdogAgentId);
|
||||
return agent ? <AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null;
|
||||
return agent ? <AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null;
|
||||
})()}
|
||||
<span className="shrink-0 max-w-40 truncate">{agentName(issue.watchdog.watchdogAgentId)}</span>
|
||||
{issue.watchdog.instructions?.trim() ? (
|
||||
|
|
@ -1185,7 +1186,7 @@ export function IssueProperties({
|
|||
const agent = (agents ?? []).find((candidate) => candidate.id === option.id);
|
||||
return (
|
||||
<>
|
||||
{agent ? <AgentIcon icon={agent.icon} className="h-3 w-3 shrink-0 text-muted-foreground" /> : null}
|
||||
{agent ? <AgentAvatar agent={agent} size={16} className="h-3 w-3 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
@ -1194,7 +1195,7 @@ export function IssueProperties({
|
|||
const agent = (agents ?? []).find((candidate) => candidate.id === option.id);
|
||||
return (
|
||||
<>
|
||||
{agent ? <AgentIcon icon={agent.icon} className="h-3 w-3 shrink-0 text-muted-foreground" /> : null}
|
||||
{agent ? <AgentAvatar agent={agent} size={16} className="h-3 w-3 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
@ -1691,7 +1692,7 @@ export function IssueProperties({
|
|||
);
|
||||
|
||||
const assigneeTrigger = assignee ? (
|
||||
<Identity name={assignee.name} size="sm" shape="square" />
|
||||
<AgentIdentity agent={assignee} size="sm" />
|
||||
) : assigneeUserLabel ? (
|
||||
<>
|
||||
<User className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
|
|
@ -1771,7 +1772,7 @@ export function IssueProperties({
|
|||
}}
|
||||
>
|
||||
{option.kind === "agent" ? (
|
||||
<AgentIcon icon={option.agent.icon} className="shrink-0 h-3 w-3 text-muted-foreground" />
|
||||
<AgentAvatar agent={option.agent} size={16} className="shrink-0 h-3 w-3 text-muted-foreground"/>
|
||||
) : option.kind === "user" ? (
|
||||
<User className="h-3 w-3 shrink-0 text-muted-foreground" />
|
||||
) : null}
|
||||
|
|
@ -1930,7 +1931,7 @@ export function IssueProperties({
|
|||
)}
|
||||
onClick={() => toggleExecutionParticipant(stageType, encoded)}
|
||||
>
|
||||
<AgentIcon icon={agent.icon} className="shrink-0 h-3 w-3 text-muted-foreground" />
|
||||
<AgentAvatar agent={agent} size={16} className="shrink-0 h-3 w-3 text-muted-foreground"/>
|
||||
{agent.name}
|
||||
</button>
|
||||
);
|
||||
|
|
@ -2873,11 +2874,7 @@ export function IssueProperties({
|
|||
to={`/agents/${originatingActor.id}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
<Identity
|
||||
name={agentName(originatingActor.id) ?? originatingActor.id.slice(0, 8)}
|
||||
size="sm"
|
||||
shape="square"
|
||||
/>
|
||||
<AgentIdentity agent={agents?.find((agent) => agent.id === originatingActor.id) ?? { id: originatingActor.id, name: agentName(originatingActor.id) ?? "Agent" }} size="sm" />
|
||||
</Link>
|
||||
) : (
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
|
|
@ -2941,7 +2938,7 @@ export function IssueProperties({
|
|||
title={`Archived by ${archivedByName} · ${formatDateTime(issue.archivedAt)}`}
|
||||
>
|
||||
{archivedByAgent
|
||||
? <AgentIcon icon={archivedByAgent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
? <AgentAvatar agent={archivedByAgent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
: null}
|
||||
<span className="min-w-0 truncate">
|
||||
{archivedByName}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentCharacter } from "../AgentCharacter";
|
||||
import { useId, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ArrowLeft, ArrowRight, Check, ChevronRight } from "lucide-react";
|
||||
|
|
@ -16,7 +17,6 @@ import {
|
|||
DialogDescription,
|
||||
DialogTitle,
|
||||
} from "../ui/dialog";
|
||||
import { PillGuy } from "../onboarding/PillGuy";
|
||||
|
||||
export type AgentBasics = {
|
||||
name: string;
|
||||
|
|
@ -166,7 +166,7 @@ export function AgentBasicsDialog({
|
|||
>
|
||||
<div className="flex min-h-0 flex-col gap-7 overflow-y-auto px-6 pb-8 sm:px-10">
|
||||
<div className="flex flex-col items-center gap-4 text-center">
|
||||
<PillGuy state="dormant" className="size-16" />
|
||||
<AgentCharacter state="sleepy" muted size={256} className="size-48" trackingScope="page" />
|
||||
<div className="space-y-2">
|
||||
<DialogTitle className="text-3xl font-semibold tracking-tight">
|
||||
{step === "name"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { AgentCharacter } from "../AgentCharacter";
|
||||
import { useAgentAppearanceDraft } from "../../hooks/useAgentAppearanceDraft";
|
||||
import { DEFAULT_CODEX_LOCAL_MODEL } from "@paperclipai/adapter-codex-local";
|
||||
import {
|
||||
SETUP_CREDENTIAL_KEYS,
|
||||
|
|
@ -49,7 +51,6 @@ import { Field } from "../agent-config-primitives";
|
|||
import { SecretPicker } from "../environment-variables-editor/SecretPicker";
|
||||
import { Button } from "../ui/button";
|
||||
import { Input } from "../ui/input";
|
||||
import { PillGuy } from "../onboarding/PillGuy";
|
||||
import {
|
||||
OnboardingCard,
|
||||
OnboardingHeading,
|
||||
|
|
@ -105,6 +106,7 @@ function Setup({
|
|||
const navigate = useNavigate();
|
||||
const cache = useQueryClient();
|
||||
const { openNewIssue } = useDialogActions();
|
||||
const appearanceDraft = useAgentAppearanceDraft(`${companyId}:new-agent`);
|
||||
const isRunner = adapterType === "paperclip_runner";
|
||||
const brandType = isRunner
|
||||
? runnerProvider === "claude"
|
||||
|
|
@ -487,6 +489,7 @@ function Setup({
|
|||
);
|
||||
const response = await agentsApi.hire(companyId, {
|
||||
name: name.trim(),
|
||||
appearance: appearanceDraft.appearance,
|
||||
role: existing.length ? "general" : "ceo",
|
||||
...(leader ? { reportsTo: leader.id } : {}),
|
||||
adapterType,
|
||||
|
|
@ -507,6 +510,7 @@ function Setup({
|
|||
setApiKey("");
|
||||
setConnection(null);
|
||||
setCreated(response.agent);
|
||||
appearanceDraft.clear();
|
||||
setScreen("saved");
|
||||
navigate(
|
||||
`/agents/new?${new URLSearchParams({ name: response.agent.name, adapterType, runnerProvider, createdAgentId: response.agent.id })}`,
|
||||
|
|
@ -614,10 +618,9 @@ function Setup({
|
|||
<MotionConfig reducedMotion="user">
|
||||
<div className="mx-auto flex max-w-5xl flex-col gap-8 py-6">
|
||||
<header className="flex items-center gap-4">
|
||||
<PillGuy
|
||||
state={created ? "alive" : "dormant"}
|
||||
className="size-14 shrink-0"
|
||||
/>
|
||||
<AgentCharacter appearance={created?.appearance ?? appearanceDraft.appearance} size={256} className="size-48" trackingScope="page"
|
||||
state={created || testState === "pass" ? "success" : testState === "running" ? "loading" : "sleepy"}
|
||||
muted={!created && testState !== "pass"} />
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight">{name}</h1>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentCharacter } from "../AgentCharacter";
|
||||
import { useState } from "react";
|
||||
import { MotionConfig } from "motion/react";
|
||||
|
||||
|
|
@ -96,7 +97,7 @@ export function ConnectModelPreview({
|
|||
{/* `relative` is load-bearing: the sleep marks anchor to this box and
|
||||
travel out past its top-right corner. */}
|
||||
<div className="relative size-(--sz-72px)">
|
||||
<PillGuy state="dormant" className="size-full" />
|
||||
<AgentCharacter state="sleepy" muted size={128} className="size-full" />
|
||||
<SleepingZs />
|
||||
</div>
|
||||
<AgentPreview agentName="Darnold" agentRole="" />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ArrowRight,
|
||||
|
|
@ -28,7 +29,6 @@ import { timeAgo } from "../../lib/timeAgo";
|
|||
import { EmptyState } from "../EmptyState";
|
||||
import { InlineEntitySelector } from "../InlineEntitySelector";
|
||||
import { DocumentAnnotationsCountChip, IssueDocumentAnnotations } from "../IssueDocumentAnnotations";
|
||||
import { AgentIcon } from "../AgentIconPicker";
|
||||
import { MarkdownEditor } from "../MarkdownEditor";
|
||||
import { ScheduleEditor, getScheduleCronValidation } from "../ScheduleEditor";
|
||||
import { RoutineVariablesEditor, RoutineVariablesHint } from "../RoutineVariablesEditor";
|
||||
|
|
@ -177,7 +177,7 @@ export function OverviewSection({
|
|||
option ? (
|
||||
currentAssignee ? (
|
||||
<>
|
||||
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={currentAssignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -193,7 +193,7 @@ export function OverviewSection({
|
|||
return (
|
||||
<>
|
||||
{assignee ? (
|
||||
<AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={assignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
) : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
ArrowRight,
|
||||
|
|
@ -27,7 +28,6 @@ import { timeAgo } from "../../lib/timeAgo";
|
|||
import { EmptyState } from "../EmptyState";
|
||||
import { InlineEntitySelector } from "../InlineEntitySelector";
|
||||
import { DocumentAnnotationsCountChip, IssueDocumentAnnotations } from "../IssueDocumentAnnotations";
|
||||
import { AgentIcon } from "../AgentIconPicker";
|
||||
import { MarkdownEditor } from "../MarkdownEditor";
|
||||
import { ScheduleEditor, getScheduleCronValidation } from "../ScheduleEditor";
|
||||
import { RoutineVariablesEditor, RoutineVariablesHint } from "../RoutineVariablesEditor";
|
||||
|
|
@ -175,7 +175,7 @@ export function OverviewSection({
|
|||
option ? (
|
||||
currentAssignee ? (
|
||||
<>
|
||||
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={currentAssignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -191,7 +191,7 @@ export function OverviewSection({
|
|||
return (
|
||||
<>
|
||||
{assignee ? (
|
||||
<AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={assignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
) : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { AgentAvatar } from "../AgentAvatar";
|
||||
import { AgentIdentity } from "../AgentIdentity";
|
||||
import { memo, type ComponentType, type SVGProps } from "react";
|
||||
import { Bot, FileText, Hexagon, MessageSquare, Paperclip, Quote } from "lucide-react";
|
||||
import type { Agent, CompanySearchResult } from "@paperclipai/shared";
|
||||
|
|
@ -46,7 +48,7 @@ function formatRelativeTime(input: string | null): string {
|
|||
|
||||
export interface SearchResultRowProps {
|
||||
result: CompanySearchResult;
|
||||
agentsById?: ReadonlyMap<string, Pick<Agent, "id" | "name">>;
|
||||
agentsById?: ReadonlyMap<string, Pick<Agent, "id" | "name" | "appearance">>;
|
||||
isActive?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
|
@ -67,9 +69,7 @@ function SearchResultRowImpl({
|
|||
className={cn(ROW_BASE, "py-3", isActive && "bg-muted/40", className)}
|
||||
data-result-type="agent"
|
||||
>
|
||||
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<Bot className="h-3 w-3" />
|
||||
</span>
|
||||
<AgentAvatar agent={agentsById?.get(result.id) ?? { id: result.id, name: result.title }} size={24} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate text-sm font-medium">{result.title}</span>
|
||||
|
|
@ -214,7 +214,7 @@ function SearchResultRowImpl({
|
|||
<div className="ml-2 hidden shrink-0 flex-col items-end gap-2 sm:flex">
|
||||
{assigneeName || updated ? (
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{assigneeName ? <Identity name={assigneeName} size="sm" /> : null}
|
||||
{assigneeName ? <AgentIdentity agent={agentsById?.get(result.issue?.assigneeAgentId ?? "") ?? { id: result.issue?.assigneeAgentId ?? undefined, name: assigneeName }} size="sm" /> : null}
|
||||
{updated ? <span className="tabular-nums">{updated}</span> : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar, type AvatarAgent } from "../AgentAvatar";
|
||||
import { useContext, useState, type ReactNode } from "react";
|
||||
import type { IssueAttachment } from "@paperclipai/shared";
|
||||
import { IssueGalleryContext } from "@/context/IssueGalleryContext";
|
||||
|
|
@ -9,7 +10,6 @@ import {
|
|||
type GalleryMediaItem,
|
||||
} from "@/components/ImageGalleryModal";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import { CommentAttributionChip } from "@/components/CommentAttributionChip";
|
||||
import {
|
||||
Attachment,
|
||||
|
|
@ -74,10 +74,12 @@ function initialsForName(name: string) {
|
|||
export function TaskChatAgentIdentity({
|
||||
agentName,
|
||||
agentIcon,
|
||||
agent,
|
||||
onBehalfOfUserName,
|
||||
}: {
|
||||
agentName: string;
|
||||
agentIcon?: string | null;
|
||||
agent?: AvatarAgent;
|
||||
onBehalfOfUserName?: string;
|
||||
}) {
|
||||
return (
|
||||
|
|
@ -85,19 +87,7 @@ export function TaskChatAgentIdentity({
|
|||
className="flex items-center gap-2 px-1"
|
||||
data-testid="task-chat-agent-identity"
|
||||
>
|
||||
<Avatar
|
||||
size="sm"
|
||||
className="shrink-0"
|
||||
data-testid="task-chat-agent-avatar"
|
||||
>
|
||||
{agentIcon ? (
|
||||
<AvatarFallback>
|
||||
<AgentIcon icon={agentIcon} className="h-3.5 w-3.5" />
|
||||
</AvatarFallback>
|
||||
) : (
|
||||
<AvatarFallback>{initialsForName(agentName)}</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
<span data-testid="task-chat-agent-avatar"><AgentAvatar agent={agent} name={agentName} size={24} /></span>
|
||||
<span className="text-sm font-semibold text-foreground">{agentName}</span>
|
||||
{onBehalfOfUserName ? (
|
||||
<CommentAttributionChip
|
||||
|
|
@ -232,7 +222,7 @@ export function TaskChatBubble({
|
|||
{!isHuman && item.authorName && !hideAgentIdentity ? (
|
||||
<TaskChatAgentIdentity
|
||||
agentName={item.authorName}
|
||||
agentIcon={item.agentIcon}
|
||||
agentIcon={item.agentIcon} agent={item.agent}
|
||||
onBehalfOfUserName={item.onBehalfOfUserName}
|
||||
/>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "../AgentAvatar";
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
|
|
@ -64,7 +65,6 @@ import {
|
|||
InlineEntitySelector,
|
||||
type InlineEntityOption,
|
||||
} from "@/components/InlineEntitySelector";
|
||||
import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import type { MentionOption } from "@/components/MarkdownEditor";
|
||||
import type { IssueAttachment, IssueWorkMode } from "@paperclipai/shared";
|
||||
|
|
@ -117,7 +117,7 @@ interface TaskChatComposerProps {
|
|||
mentions?: MentionOption[];
|
||||
enableReassign?: boolean;
|
||||
reassignOptions?: InlineEntityOption[];
|
||||
agentMap?: ReadonlyMap<string, { icon?: string | null }>;
|
||||
agentMap?: ReadonlyMap<string, import("../AgentAvatar").AvatarAgent & { icon?: string | null }>;
|
||||
userProfileMap?: ReadonlyMap<
|
||||
string,
|
||||
{ label: string; image: string | null }
|
||||
|
|
@ -239,7 +239,7 @@ function AssigneeIdentityAvatar({
|
|||
}: {
|
||||
assigneeValue: string;
|
||||
label: string;
|
||||
agentMap: ReadonlyMap<string, { icon?: string | null }> | undefined;
|
||||
agentMap: ReadonlyMap<string, import("../AgentAvatar").AvatarAgent & { icon?: string | null }> | undefined;
|
||||
userProfileMap:
|
||||
| ReadonlyMap<string, { label: string; image: string | null }>
|
||||
| null
|
||||
|
|
@ -250,8 +250,7 @@ function AssigneeIdentityAvatar({
|
|||
const agentId = assigneeValue.slice("agent:".length);
|
||||
const icon = agentMap?.get(agentId)?.icon ?? "bot";
|
||||
return (
|
||||
<Avatar
|
||||
size="xs"
|
||||
<span
|
||||
className="shrink-0"
|
||||
data-assignee-identity={assigneeValue}
|
||||
data-assignee-trigger-icon={placement === "trigger" ? icon : undefined}
|
||||
|
|
@ -259,10 +258,8 @@ function AssigneeIdentityAvatar({
|
|||
placement === "option" ? assigneeValue : undefined
|
||||
}
|
||||
>
|
||||
<AvatarFallback>
|
||||
<AgentIcon icon={icon} className="h-3 w-3" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<AgentAvatar agent={agentMap?.get(agentId) ?? { id: agentId }} size={16} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ describe("TaskChatDescriptionBubble (PAP-375)", () => {
|
|||
expect(bubble).not.toBeNull();
|
||||
expect(bubble?.getAttribute("data-author")).toBe("human");
|
||||
expect(bubble?.className).toContain("items-end");
|
||||
expect(bubble?.querySelector('[data-testid="task-chat-agent-avatar"]')).toBeNull();
|
||||
expect(bubble?.querySelector('[data-slot="agent-avatar"]')).toBeNull();
|
||||
const body = bubble?.querySelector(".bg-\\(--liveness-blue\\)");
|
||||
expect(body).not.toBeNull();
|
||||
expect(body?.textContent).toContain("Ship the widget by");
|
||||
|
|
@ -94,7 +94,7 @@ describe("TaskChatDescriptionBubble (PAP-375)", () => {
|
|||
const bubble = container.querySelector('[data-testid="task-chat-description-bubble"]');
|
||||
expect(bubble?.getAttribute("data-author")).toBe("agent");
|
||||
expect(bubble?.className).toContain("items-start");
|
||||
expect(bubble?.querySelector('[data-testid="task-chat-agent-avatar"]')).not.toBeNull();
|
||||
expect(bubble?.querySelector('[data-slot="agent-avatar"]')).not.toBeNull();
|
||||
expect(bubble?.textContent).toContain("CEO");
|
||||
expect(bubble?.querySelector(".bg-\\(--bubble-agent\\)")).not.toBeNull();
|
||||
expect(bubble?.querySelector(".bg-\\(--liveness-blue\\)")).toBeNull();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar, type AvatarAgent } from "../AgentAvatar";
|
||||
import { useState } from "react";
|
||||
import { Pencil } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -5,7 +6,6 @@ import { FoldCurtain } from "@/components/FoldCurtain";
|
|||
import { InlineEditor } from "@/components/InlineEditor";
|
||||
import { MarkdownBody, type MarkdownExternalReferenceMap } from "@/components/MarkdownBody";
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import type { MentionOption } from "@/components/MarkdownEditor";
|
||||
import { formatTaskChatTimestamp } from "./task-chat-adapter";
|
||||
|
||||
|
|
@ -23,6 +23,7 @@ export interface TaskChatIssueBrief {
|
|||
authorName?: string;
|
||||
/** Creating agent's assigned icon (AgentIconName) for the avatar header. */
|
||||
agentIcon?: string | null;
|
||||
agent?: AvatarAgent;
|
||||
/** issue.createdAt — rendered like the other bubbles' footer timestamps. */
|
||||
createdAt?: string | Date;
|
||||
onSave: (description: string) => void | Promise<unknown>;
|
||||
|
|
@ -115,15 +116,7 @@ export function TaskChatDescriptionBubble({ brief }: TaskChatDescriptionBubblePr
|
|||
>
|
||||
{!isHuman && brief.authorName ? (
|
||||
<span className="flex items-center gap-2 px-1">
|
||||
<Avatar size="sm" className="shrink-0" data-testid="task-chat-agent-avatar">
|
||||
{brief.agentIcon ? (
|
||||
<AvatarFallback>
|
||||
<AgentIcon icon={brief.agentIcon} className="h-3.5 w-3.5" />
|
||||
</AvatarFallback>
|
||||
) : (
|
||||
<AvatarFallback>{initialsForName(brief.authorName)}</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
<AgentAvatar agent={brief.agent} name={brief.authorName} size={24} />
|
||||
<span className="text-sm font-semibold text-foreground">{brief.authorName}</span>
|
||||
</span>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -429,6 +429,7 @@ export function TaskChatRunnerTurn({
|
|||
runId,
|
||||
agentName,
|
||||
agentIcon,
|
||||
agent,
|
||||
items,
|
||||
status,
|
||||
execution,
|
||||
|
|
@ -443,6 +444,7 @@ export function TaskChatRunnerTurn({
|
|||
runId?: string | null;
|
||||
agentName?: string | null;
|
||||
agentIcon?: string | null;
|
||||
agent?: import("../AgentAvatar").AvatarAgent;
|
||||
items: readonly TaskChatItem[];
|
||||
status: string;
|
||||
execution?: ExecutionProjection | null;
|
||||
|
|
@ -526,7 +528,7 @@ export function TaskChatRunnerTurn({
|
|||
data-testid="task-chat-runner-identity-row"
|
||||
>
|
||||
{agentName ? (
|
||||
<TaskChatAgentIdentity agentName={agentName} agentIcon={agentIcon} />
|
||||
<TaskChatAgentIdentity agentName={agentName} agentIcon={agentIcon} agent={agent} />
|
||||
) : null}
|
||||
<RunnerTurnStatus
|
||||
status={status}
|
||||
|
|
@ -602,7 +604,7 @@ export function TaskChatRunnerTurn({
|
|||
data-testid="task-chat-final-response"
|
||||
>
|
||||
<TaskChatBubble
|
||||
item={{ ...final, authorName: agentName ?? undefined, agentIcon, timestamp: final.timestamp ?? formatTaskChatTimestamp(final.atMs) }}
|
||||
item={{ ...final, authorName: agentName ?? undefined, agentIcon, agent, timestamp: final.timestamp ?? formatTaskChatTimestamp(final.atMs) }}
|
||||
animateEntry={false}
|
||||
hideAgentIdentity={!continuedAfterSteering}
|
||||
actions={<TaskChatBubbleActions copyText={final.text} />}
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ function renderItem(
|
|||
...item.attachedTurn,
|
||||
agentName: item.attachedTurn.agentName ?? item.authorName,
|
||||
agentIcon: item.attachedTurn.agentIcon ?? item.agentIcon,
|
||||
agent: item.attachedTurn.agent ?? item.agent,
|
||||
}
|
||||
: item.attachedTurn;
|
||||
const turn = attachedTurnItem ? (
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ export function TaskChatTurn({
|
|||
{item.agentName ? (
|
||||
<TaskChatAgentIdentity
|
||||
agentName={item.agentName}
|
||||
agentIcon={item.agentIcon}
|
||||
agentIcon={item.agentIcon} agent={item.agent}
|
||||
/>
|
||||
) : null}
|
||||
<span className="min-w-0 truncate">
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ export function commentsToTaskChatItems(
|
|||
kind: "message",
|
||||
author: kind,
|
||||
authorName,
|
||||
agent: effectiveAgentId(comment) ? ctx.agentMap?.get(effectiveAgentId(comment)!) ?? { id: effectiveAgentId(comment)! } : undefined,
|
||||
text: comment.body,
|
||||
timestamp: formatTaskChatCommentTimestamp(comment, kind),
|
||||
optimistic,
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ export interface TaskChatMessageItem {
|
|||
}>;
|
||||
/** Assigned agent icon name (AgentIconName) for the avatar header. */
|
||||
agentIcon?: string | null;
|
||||
agent?: import("../AgentAvatar").AvatarAgent;
|
||||
/**
|
||||
* Responsible user's display name, set only when this agent comment is a
|
||||
* cross-issue write (the author is not the assignee). Renders as a
|
||||
|
|
@ -502,6 +503,7 @@ export interface TaskChatTurnItem {
|
|||
/** Agent identity retained when a live runner turn becomes durable history. */
|
||||
agentName?: string;
|
||||
agentIcon?: string | null;
|
||||
agent?: import("../AgentAvatar").AvatarAgent;
|
||||
/**
|
||||
* The in-flight run's status line, hoisted to be THE turn's single visible
|
||||
* row while collapsed (PAP-354 parent-row model). Absent once settled.
|
||||
|
|
|
|||
|
|
@ -178,13 +178,15 @@ describe("WorkTimelineChart", () => {
|
|||
expect(lastCall?.toMs).toBeCloseTo(new Date("2026-07-02T01:00:00.000Z").getTime(), -3);
|
||||
});
|
||||
|
||||
it("renders configured agent icons in the actor gutter instead of generated initials", () => {
|
||||
it("renders cached persona images in the actor gutter", () => {
|
||||
renderChart(timelineSample());
|
||||
|
||||
const gutter = container.querySelector<SVGSVGElement>("[data-testid='work-timeline-actor-gutter']");
|
||||
|
||||
expect(gutter?.querySelector(".lucide-code")).not.toBeNull();
|
||||
expect(gutter?.querySelector(".lucide-shield")).not.toBeNull();
|
||||
const portraits = gutter?.querySelectorAll('image[data-testid="timeline-agent-icon"]');
|
||||
expect(portraits?.length).toBe(2);
|
||||
expect(portraits?.[0].getAttribute("href")).toContain("/api/agent-avatars/cap-v1/");
|
||||
expect(portraits?.[1].getAttribute("href")).toContain("/api/agent-avatars/cap-v1/");
|
||||
expect(gutter?.textContent).not.toContain("CC");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { agentAvatarUrl, resolveAgentAppearance } from "@paperclipai/shared";
|
||||
/**
|
||||
* Work Timeline — custom-SVG Gantt (board-locked Direction C, PAP-12422).
|
||||
*
|
||||
|
|
@ -12,7 +13,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|||
import { useLocation } from "@/lib/router";
|
||||
import type { WorkTimelineActor, WorkTimelineResult } from "@paperclipai/shared";
|
||||
import { applyCompanyPrefix, extractCompanyPrefixFromPath } from "@/lib/company-routes";
|
||||
import { getAgentIcon } from "@/lib/agent-icons";
|
||||
import {
|
||||
AXIS_H,
|
||||
actorType,
|
||||
|
|
@ -182,19 +182,10 @@ function ActorGlyph({
|
|||
clipId: string;
|
||||
}) {
|
||||
if (actor.type === "agent") {
|
||||
const Icon = getAgentIcon(actor.avatar);
|
||||
const size = r > 10 ? 16 : 13;
|
||||
return (
|
||||
<Icon
|
||||
data-testid="timeline-agent-icon"
|
||||
x={cx - size / 2}
|
||||
y={cy - size / 2}
|
||||
width={size}
|
||||
height={size}
|
||||
strokeWidth={2.2}
|
||||
color="var(--color-muted-foreground)"
|
||||
/>
|
||||
);
|
||||
const size = r > 10 ? 24 : 16;
|
||||
const appearance = resolveAgentAppearance(actor.appearance, actor.id.replace(/^agent:/, ""));
|
||||
return <image data-testid="timeline-agent-icon" href={agentAvatarUrl(appearance, size, 2)}
|
||||
x={cx - size / 2} y={cy - size / 2} width={size} height={size} preserveAspectRatio="xMidYMid meet" />;
|
||||
}
|
||||
|
||||
const stroke = "var(--color-foreground)";
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import {
|
|||
type ModelSource,
|
||||
} from "./components/onboarding/ModelSourceTiles";
|
||||
import { OnboardingHeading } from "./components/onboarding/OnboardingPrimitives";
|
||||
import { PillGuy } from "./components/onboarding/PillGuy";
|
||||
import { AgentCharacter } from "./components/AgentCharacter";
|
||||
import { SleepingZs } from "./components/onboarding/SleepingZs";
|
||||
import { Stepper } from "./components/onboarding/Stepper";
|
||||
import "./index.css";
|
||||
|
|
@ -299,7 +299,7 @@ function ConnectFlowPreview({
|
|||
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative size-(--sz-72px)">
|
||||
<PillGuy state={done ? "alive" : "dormant"} className="size-full" />
|
||||
<AgentCharacter muted={!done} state={done ? "success" : "sleepy"} size={128} className="size-full" />
|
||||
{!done && <SleepingZs />}
|
||||
</div>
|
||||
<AgentPreview agentName="Ron" agentRole="" />
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ import { ApiError } from "@/api/client";
|
|||
import { toolsApi } from "@/api/tools";
|
||||
import { agentsApi } from "@/api/agents";
|
||||
import { appCopyFor, credentialFieldLabel } from "@/lib/app-gallery-copy";
|
||||
import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import { AgentMultiSelect } from "@/components/AgentMultiSelect";
|
||||
import { InlineBanner } from "@/components/InlineBanner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export function buildCompanyUserMentionOptions(
|
|||
}
|
||||
|
||||
export function isAgentTaskTarget(
|
||||
agent: Pick<Agent, "status"> & Partial<Pick<Agent, "orgChainHealth">>,
|
||||
agent: Pick<Agent, "status"> & Partial<Pick<Agent, "orgChainHealth" | "appearance">>,
|
||||
): boolean {
|
||||
return (
|
||||
agent.status !== "terminated" &&
|
||||
|
|
@ -116,7 +116,7 @@ export function buildIssueMentionOptions(
|
|||
}
|
||||
|
||||
export function buildMarkdownMentionOptions(args: {
|
||||
agents?: Array<Pick<Agent, "id" | "name" | "status" | "icon"> & Partial<Pick<Agent, "orgChainHealth">>> | null | undefined;
|
||||
agents?: Array<Pick<Agent, "id" | "name" | "status" | "icon"> & Partial<Pick<Agent, "orgChainHealth" | "appearance">>> | null | undefined;
|
||||
projects?: Array<Pick<Project, "id" | "name" | "color">> | null | undefined;
|
||||
members?: CompanyUserRecord[] | null | undefined;
|
||||
issues?: Array<Pick<Issue, "id" | "identifier" | "title">> | null | undefined;
|
||||
|
|
@ -132,6 +132,7 @@ export function buildMarkdownMentionOptions(args: {
|
|||
kind: "agent" as const,
|
||||
agentId: agent.id,
|
||||
agentIcon: agent.icon,
|
||||
agentAppearance: agent.appearance,
|
||||
})),
|
||||
...[...(args.projects ?? [])]
|
||||
.sort((left, right) => left.name.localeCompare(right.name))
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { AgentCharacter } from "../components/AgentCharacter";
|
||||
import { characterStateForAgent } from "@paperclipai/shared";
|
||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||
import { useParams, useNavigate, Link, Navigate, useBeforeUnload, type NavigateFunction } from "@/lib/router";
|
||||
import { useQuery, useMutation, useQueryClient, type QueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -89,7 +91,6 @@ import {
|
|||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker";
|
||||
import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView";
|
||||
import { AgentToolsTab } from "./AgentToolsTab";
|
||||
import { AgentChannelsPanel } from "../components/chat/AgentChannelsPanel";
|
||||
|
|
@ -1280,14 +1281,7 @@ export function AgentDetail() {
|
|||
{/* Header */}
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<AgentIconPicker
|
||||
value={agent.icon}
|
||||
onChange={(icon) => updateIcon.mutate(icon)}
|
||||
>
|
||||
<button className="shrink-0 flex items-center justify-center h-12 w-12 rounded-lg bg-accent hover:bg-accent/80 transition-colors">
|
||||
<AgentIcon icon={agent.icon} className="h-6 w-6" />
|
||||
</button>
|
||||
</AgentIconPicker>
|
||||
<AgentCharacter agent={agent} state={characterStateForAgent(agent.status)} size={96} trackingScope="page" />
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-2xl font-bold truncate">{agent.name}</h2>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { AgentCharacter } from "../components/AgentCharacter";
|
||||
import { characterStateForAgent } from "@paperclipai/shared";
|
||||
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
|
||||
import { useParams, useNavigate, Link, Navigate, useBeforeUnload, type NavigateFunction } from "@/lib/router";
|
||||
import { useQuery, useMutation, useQueryClient, type QueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -25,7 +27,6 @@ import { queryKeys } from "../lib/queryKeys";
|
|||
import { copyTextToClipboard } from "../lib/clipboard";
|
||||
import { AgentSkillsTab } from "./agent-skills/AgentSkillsTab";
|
||||
import { AgentConfigForm } from "../components/AgentConfigForm";
|
||||
import { PillGuy } from "../components/onboarding/PillGuy";
|
||||
import { getAdapterDisplay } from "../adapters/adapter-display-registry";
|
||||
import { adapterLabels, roleLabels, help } from "../components/agent-config-primitives";
|
||||
import { ToggleSwitch } from "@/components/ui/toggle-switch";
|
||||
|
|
@ -87,7 +88,6 @@ import {
|
|||
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
|
||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { AgentIcon, AgentIconPicker } from "../components/AgentIconPicker";
|
||||
import { RunTranscriptView, type TranscriptMode } from "../components/transcript/RunTranscriptView";
|
||||
import { AgentToolsTab } from "./AgentToolsTab";
|
||||
import { AgentChannelsPanel } from "../components/chat/AgentChannelsPanel";
|
||||
|
|
@ -1245,7 +1245,7 @@ export function AgentDetail() {
|
|||
<header className="flex flex-wrap items-center justify-between gap-5 border-b border-border pb-6">
|
||||
<div className="flex min-w-0 items-center gap-4">
|
||||
<div role="img" aria-label={`${agent.name} avatar`} className="shrink-0">
|
||||
<PillGuy state="alive" className="size-12" />
|
||||
<AgentCharacter agent={agent} state={characterStateForAgent(agent.status)} size={96} trackingScope="page" />
|
||||
</div>
|
||||
<div className="min-w-0 space-y-1">
|
||||
<h1 className="truncate text-2xl font-semibold tracking-tight">{agent.name}</h1>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useState, useEffect, useMemo, lazy, Suspense } from "react";
|
||||
import { Link, useNavigate, useLocation } from "@/lib/router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
|
@ -402,7 +403,7 @@ export function Agents() {
|
|||
leading={hasInvalidOrgChain ? (
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" aria-label="Invalid reporting chain" />
|
||||
) : (
|
||||
<AgentStatusCapsule status={agent.status} />
|
||||
<AgentAvatar agent={agent} size={32} />
|
||||
)}
|
||||
secondaryRow={
|
||||
builtInCluster ? (
|
||||
|
|
@ -655,7 +656,7 @@ function OrgTreeNode({
|
|||
{hasInvalidOrgChain ? (
|
||||
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-amber-500" aria-label="Invalid reporting chain" />
|
||||
) : (
|
||||
<AgentStatusCapsule status={node.status} />
|
||||
<AgentAvatar agent={agent ?? node} size={24} />
|
||||
)}
|
||||
<div className="flex-1 min-w-0 flex flex-wrap items-center gap-2">
|
||||
{/* Name floor + `truncate` keeps the primary identifier readable; the
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useState, useEffect, useMemo, lazy, Suspense } from "react";
|
||||
import { Link, useNavigate, useLocation } from "@/lib/router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
|
@ -398,7 +399,7 @@ export function Agents({ initialView = "list" }: { initialView?: AgentsView } =
|
|||
leading={hasInvalidOrgChain ? (
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-amber-500" aria-label="Invalid reporting chain" />
|
||||
) : (
|
||||
<AgentStatusCapsule status={agent.status} />
|
||||
<AgentAvatar agent={agent} size={32} />
|
||||
)}
|
||||
secondaryRow={builtInCluster && (
|
||||
<div className="@5xl:hidden flex flex-wrap items-center gap-1.5">
|
||||
|
|
@ -633,7 +634,7 @@ function OrgTreeNode({
|
|||
{hasInvalidOrgChain ? (
|
||||
<AlertTriangle className="h-3.5 w-3.5 shrink-0 text-amber-500" aria-label="Invalid reporting chain" />
|
||||
) : (
|
||||
<AgentStatusCapsule status={node.status} />
|
||||
<AgentAvatar agent={agent ?? node} size={24} />
|
||||
)}
|
||||
<div className="flex-1 min-w-0 flex flex-wrap items-center gap-2">
|
||||
{/* Name floor + `truncate` keeps the primary identifier readable; the
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Link, useNavigate, useParams, useSearchParams } from "@/lib/router";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -213,8 +214,8 @@ export function ApprovalDetail() {
|
|||
{approval.requestedByAgentId && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs">Requested by</span>
|
||||
<Identity
|
||||
name={agentNameById.get(approval.requestedByAgentId) ?? approval.requestedByAgentId.slice(0, 8)}
|
||||
<AgentIdentity
|
||||
agent={agents?.find((agent) => agent.id === approval.requestedByAgentId) ?? { id: approval.requestedByAgentId, name: "Agent" }}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -331,8 +332,8 @@ export function ApprovalDetail() {
|
|||
<div className="flex items-center justify-between mb-1">
|
||||
{comment.authorAgentId ? (
|
||||
<Link to={`/agents/${comment.authorAgentId}`} className="hover:underline">
|
||||
<Identity
|
||||
name={agentNameById.get(comment.authorAgentId) ?? comment.authorAgentId.slice(0, 8)}
|
||||
<AgentIdentity
|
||||
agent={agents?.find((agent) => agent.id === comment.authorAgentId) ?? { id: comment.authorAgentId, name: "Agent" }}
|
||||
size="sm"
|
||||
/>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import {
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
|
|
@ -29,7 +30,6 @@ import {
|
|||
AgentBubbleActionRow,
|
||||
agentBubbleDateLabel,
|
||||
} from "../components/AgentBubbleActionRow";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { cn, formatDateTime } from "../lib/utils";
|
||||
import type { FeedbackVoteValue } from "@paperclipai/shared";
|
||||
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
||||
|
|
@ -63,21 +63,8 @@ function agentInitials(name: string): string {
|
|||
* Icon-adjacent-to-name header rendered directly above an agent bubble —
|
||||
* the shared `[agent icon][agent name]` convention (PAP-105 / PAP-97).
|
||||
*/
|
||||
function AgentBubbleHeader({ name, icon }: { name: string; icon: string | null }) {
|
||||
return (
|
||||
<div className="mb-1 flex items-center gap-1.5 pl-1">
|
||||
<Avatar size="sm" className="shrink-0">
|
||||
<AvatarFallback>
|
||||
{icon ? (
|
||||
<AgentIcon icon={icon} className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
agentInitials(name)
|
||||
)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm font-medium text-foreground">{name}</span>
|
||||
</div>
|
||||
);
|
||||
function AgentBubbleHeader({ agent }: { agent: import("../components/AgentAvatar").AvatarAgent }) {
|
||||
return <div className="mb-1 flex items-center gap-1.5 pl-1"><AgentAvatar agent={agent} size={24} /><span className="text-sm font-medium text-foreground">{agent.name}</span></div>;
|
||||
}
|
||||
|
||||
/** Agent-styled chat bubble containing the three-dot typing indicator. */
|
||||
|
|
@ -776,7 +763,7 @@ export function BoardChat() {
|
|||
return (
|
||||
<>
|
||||
<div className="flex flex-col items-start">
|
||||
<AgentBubbleHeader name={ceoName} icon={ceoAgent.icon} />
|
||||
<AgentBubbleHeader agent={{ ...ceoAgent, name: ceoName }} />
|
||||
<div
|
||||
className={cn(
|
||||
boardChatBubbleShell,
|
||||
|
|
@ -833,7 +820,7 @@ export function BoardChat() {
|
|||
const agentIconValue = agent?.icon ?? null;
|
||||
return (
|
||||
<div key={comment.id} className="flex flex-col items-start">
|
||||
<AgentBubbleHeader name={agentName} icon={agentIconValue} />
|
||||
<AgentBubbleHeader agent={agent ?? { id: comment.authorAgentId ?? undefined, name: agentName }} />
|
||||
<div
|
||||
className={cn(
|
||||
boardChatBubbleShell,
|
||||
|
|
@ -883,7 +870,7 @@ export function BoardChat() {
|
|||
{streamingText && (
|
||||
<div className="flex flex-col items-start">
|
||||
{ceoAgent && (
|
||||
<AgentBubbleHeader name={ceoAgent.name} icon={ceoAgent.icon} />
|
||||
<AgentBubbleHeader agent={ceoAgent} />
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useEffect, useMemo, useRef, useState, type SVGProps } from "react";
|
||||
import { Link, useNavigate, useParams, useSearchParams } from "@/lib/router";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -35,7 +37,6 @@ import { MarkdownEditor } from "../components/MarkdownEditor";
|
|||
import { PageSkeleton } from "../components/PageSkeleton";
|
||||
import { CopyText } from "../components/CopyText";
|
||||
import { Identity } from "../components/Identity";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { AgentMultiSelect } from "../components/AgentMultiSelect";
|
||||
import { useAdapterCapabilities } from "../adapters/use-adapter-capabilities";
|
||||
import {
|
||||
|
|
@ -3242,7 +3243,7 @@ export function SkillDetailPage({
|
|||
const meta = attachAgentMetaById.get(agent.id);
|
||||
return (
|
||||
<div key={agent.id} className="flex items-center gap-3 border-b border-border py-3 text-sm last:border-b-0">
|
||||
<AgentIcon icon={meta?.icon ?? null} className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={meta} size={16} className="h-4 w-4 shrink-0 text-muted-foreground"/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate font-medium">{agent.name}</span>
|
||||
|
|
@ -3450,7 +3451,7 @@ export function SkillDetailPage({
|
|||
to={`/agents/${agent.urlKey}/skills`}
|
||||
className="flex items-center gap-2 rounded-md px-1.5 py-1 text-sm no-underline hover:bg-accent/40"
|
||||
>
|
||||
<AgentIcon icon={meta?.icon ?? null} className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={meta} size={16} className="h-4 w-4 shrink-0 text-muted-foreground"/>
|
||||
<span className="min-w-0 flex-1 truncate text-foreground">{agent.name}</span>
|
||||
{meta?.paused ? (
|
||||
<Pause className="h-3 w-3 shrink-0 text-amber-500" aria-label="Paused" />
|
||||
|
|
@ -3910,7 +3911,7 @@ function SkillPane({
|
|||
to={`/agents/${agent.urlKey}/skills`}
|
||||
className="group rounded-md border border-transparent p-2 no-underline hover:border-border hover:bg-accent/40"
|
||||
>
|
||||
<Identity name={agent.name} size="sm" />
|
||||
<AgentIdentity agent={agent} size="sm" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useEffect, useMemo, useRef, useState, type SVGProps } from "react";
|
||||
import { Link, useNavigate, useParams, useSearchParams } from "@/lib/router";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
|
@ -35,7 +37,6 @@ import { MarkdownEditor } from "../components/MarkdownEditor";
|
|||
import { PageSkeleton } from "../components/PageSkeleton";
|
||||
import { CopyText } from "../components/CopyText";
|
||||
import { Identity } from "../components/Identity";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { AgentMultiSelect } from "../components/AgentMultiSelect";
|
||||
import { useAdapterCapabilities } from "../adapters/use-adapter-capabilities";
|
||||
import { useStreamlinedUiEnabled } from "../hooks/useStreamlinedUiEnabled";
|
||||
|
|
@ -3259,7 +3260,7 @@ export function SkillDetailPage({
|
|||
const meta = attachAgentMetaById.get(agent.id);
|
||||
return (
|
||||
<div key={agent.id} className="flex items-center gap-3 border-b border-border py-3 text-sm last:border-b-0">
|
||||
<AgentIcon icon={meta?.icon ?? null} className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={meta} size={16} className="h-4 w-4 shrink-0 text-muted-foreground"/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="truncate font-medium">{agent.name}</span>
|
||||
|
|
@ -3467,7 +3468,7 @@ export function SkillDetailPage({
|
|||
to={`/agents/${agent.urlKey}/skills`}
|
||||
className="flex items-center gap-2 rounded-md px-1.5 py-1 text-sm no-underline hover:bg-accent/40"
|
||||
>
|
||||
<AgentIcon icon={meta?.icon ?? null} className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={meta} size={16} className="h-4 w-4 shrink-0 text-muted-foreground"/>
|
||||
<span className="min-w-0 flex-1 truncate text-foreground">{agent.name}</span>
|
||||
{meta?.paused ? (
|
||||
<Pause className="h-3 w-3 shrink-0 text-amber-500" aria-label="Paused" />
|
||||
|
|
@ -3928,7 +3929,7 @@ function SkillPane({
|
|||
to={`/agents/${agent.urlKey}/skills`}
|
||||
className="group rounded-md border border-transparent p-2 no-underline hover:border-border hover:bg-accent/40"
|
||||
>
|
||||
<Identity name={agent.name} size="sm" />
|
||||
<AgentIdentity agent={agent} size="sm" />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { useEffect, useMemo, useRef, useState, type ComponentType } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type {
|
||||
|
|
@ -742,7 +743,7 @@ export function Costs() {
|
|||
) : (
|
||||
<span className="h-3 w-3 shrink-0" />
|
||||
)}
|
||||
<Identity name={row.agentName ?? row.agentId} size="sm" />
|
||||
<AgentIdentity agent={{ id: row.agentId, name: row.agentName ?? row.agentId, appearance: row.agentAppearance }} size="sm" />
|
||||
{row.agentStatus === "terminated" ? <StatusBadge status="terminated" /> : null}
|
||||
</div>
|
||||
<div className="text-right text-sm tabular-nums">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { useEffect, useMemo, useRef, useState, type ComponentType } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type {
|
||||
|
|
@ -770,7 +771,7 @@ export function Costs({
|
|||
) : (
|
||||
<span className="h-3 w-3 shrink-0" />
|
||||
)}
|
||||
<Identity name={row.agentName ?? row.agentId} size="sm" />
|
||||
<AgentIdentity agent={{ id: row.agentId, name: row.agentName ?? row.agentId, appearance: row.agentAppearance }} size="sm" />
|
||||
{row.agentStatus === "terminated" ? <StatusBadge status="terminated" /> : null}
|
||||
</div>
|
||||
<div className="text-right text-sm tabular-nums">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "../components/AgentIdentity";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLocation } from "@/lib/router";
|
||||
import {
|
||||
|
|
@ -538,7 +539,7 @@ export function Dashboard() {
|
|||
{issue.assigneeAgentId && (() => {
|
||||
const name = agentName(issue.assigneeAgentId);
|
||||
return name
|
||||
? <span className="hidden sm:inline-flex"><Identity name={name} size="sm" /></span>
|
||||
? <span className="hidden sm:inline-flex"><AgentIdentity agent={agents?.find(agent => agent.id === issue.assigneeAgentId) ?? { id: issue.assigneeAgentId ?? undefined, name }} size="sm" /></span>
|
||||
: null;
|
||||
})()}
|
||||
<span className="text-xs text-muted-foreground sm:hidden">·</span>
|
||||
|
|
|
|||
|
|
@ -2693,7 +2693,7 @@ function StreamlinedInbox() {
|
|||
<BlockedInboxView
|
||||
companyId={selectedCompanyId!}
|
||||
searchQuery={searchQuery}
|
||||
agentNameById={agentById}
|
||||
agentNameById={agentById} agents={agents}
|
||||
userLabelById={companyUserLabelMap}
|
||||
issueLinkState={issueLinkState}
|
||||
groupBy={blockedGroupBy}
|
||||
|
|
@ -2906,6 +2906,8 @@ function StreamlinedInbox() {
|
|||
defaultProjectWorkspaceIdByProjectId,
|
||||
})}
|
||||
assigneeName={agentName(issue.assigneeAgentId)}
|
||||
assigneeAgent={agents?.find((agent) => agent.id === issue.assigneeAgentId)}
|
||||
creatorAgent={agents?.find((agent) => agent.id === issue.createdByAgentId)}
|
||||
assigneeUserName={
|
||||
formatAssigneeUserLabel(issue.assigneeUserId, currentUserId, companyUserLabelMap)
|
||||
?? assigneeUserProfile?.label
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { TaskChatScrollNavigation } from "@/components/task-chat/scroll-navigation";
|
||||
import {
|
||||
memo,
|
||||
|
|
@ -187,7 +189,6 @@ import {
|
|||
import { IssueSiblingNavigation } from "../components/IssueSiblingNavigation";
|
||||
import type { MarkdownExternalReferenceMap } from "../components/MarkdownBody";
|
||||
import { IssuesList } from "../components/IssuesList";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { IssueReferenceActivitySummary } from "../components/IssueReferenceActivitySummary";
|
||||
import { IssueFieldChangeReceipt } from "../components/IssueFieldChangeReceipt";
|
||||
import { IssueWriteDenialNotice } from "../components/IssueWriteDenialNotice";
|
||||
|
|
@ -655,7 +656,7 @@ function ActorIdentity({
|
|||
const id = evt.actorId;
|
||||
if (evt.actorType === "agent") {
|
||||
const agent = agentMap.get(id);
|
||||
return <Identity name={agent?.name ?? id.slice(0, 8)} size="sm" />;
|
||||
return <AgentIdentity agent={agent ?? { id, name: id.slice(0, 8) }} size="sm" />;
|
||||
}
|
||||
if (evt.actorType === "system") return <Identity name="System" size="sm" />;
|
||||
if (evt.actorType === "user") {
|
||||
|
|
@ -672,6 +673,7 @@ function ActorIdentity({
|
|||
}
|
||||
|
||||
export type AttributionActor = {
|
||||
appearance?: Agent["appearance"];
|
||||
kind: "agent" | "user";
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -702,36 +704,26 @@ function AttributionAvatar({
|
|||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Avatar
|
||||
size="xs"
|
||||
shape={actor.kind === "agent" ? "square" : "circle"}
|
||||
aria-label={accessibleLabel}
|
||||
data-testid={`issue-${testIdLabel}-avatar`}
|
||||
className="ring-2 ring-background"
|
||||
>
|
||||
{actor.avatarUrl ? (
|
||||
<AvatarImage src={actor.avatarUrl} alt="" />
|
||||
) : null}
|
||||
<AvatarFallback>{attributionInitials(actor.name)}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span aria-label={accessibleLabel} data-testid={`issue-${testIdLabel}-avatar`}>
|
||||
{actor.kind === "agent" ? <AgentAvatar agent={actor} size={20} /> : (
|
||||
<Avatar size="xs" className="ring-2 ring-background">
|
||||
{actor.avatarUrl ? <AvatarImage src={actor.avatarUrl} alt="" /> : null}
|
||||
<AvatarFallback>{attributionInitials(actor.name)}</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={6} className="px-2 py-1.5">
|
||||
<div
|
||||
className="flex items-center gap-2"
|
||||
data-testid={`issue-${testIdLabel}-tooltip`}
|
||||
>
|
||||
<Avatar
|
||||
size="sm"
|
||||
shape={actor.kind === "agent" ? "square" : "circle"}
|
||||
className="ring-1 ring-background/30"
|
||||
>
|
||||
{actor.avatarUrl ? (
|
||||
<AvatarImage src={actor.avatarUrl} alt="" />
|
||||
) : null}
|
||||
<AvatarFallback className="bg-background/20 text-background">
|
||||
{attributionInitials(actor.name)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
{actor.kind === "agent" ? <AgentAvatar agent={actor} size={32} /> : (
|
||||
<Avatar size="sm" className="ring-1 ring-background/30">
|
||||
{actor.avatarUrl ? <AvatarImage src={actor.avatarUrl} alt="" /> : null}
|
||||
<AvatarFallback>{attributionInitials(actor.name)}</AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<div className="text-(length:--text-nano) font-medium uppercase leading-none text-background/70">
|
||||
{label}
|
||||
|
|
@ -769,6 +761,7 @@ function IssueAttributionByline({
|
|||
? {
|
||||
kind: "agent",
|
||||
id: issue.assigneeAgentId,
|
||||
appearance: agentMap.get(issue.assigneeAgentId)?.appearance,
|
||||
name:
|
||||
agentMap.get(issue.assigneeAgentId)?.name ??
|
||||
issue.assigneeAgentId.slice(0, 8),
|
||||
|
|
@ -790,6 +783,7 @@ function IssueAttributionByline({
|
|||
? {
|
||||
kind: "agent",
|
||||
id: originatingActor.id,
|
||||
appearance: agentMap.get(originatingActor.id)?.appearance,
|
||||
name:
|
||||
agentMap.get(originatingActor.id)?.name ??
|
||||
originatingActor.id.slice(0, 8),
|
||||
|
|
@ -7698,7 +7692,8 @@ export function IssueDetail() {
|
|||
? (agentMap.get(issue.createdByAgentId)?.name ??
|
||||
"Agent")
|
||||
: undefined,
|
||||
agentIcon: issue.createdByAgentId
|
||||
agent: issue.createdByAgentId ? agentMap.get(issue.createdByAgentId) ?? { id: issue.createdByAgentId } : undefined,
|
||||
agentIcon: issue.createdByAgentId
|
||||
? agentMap.get(issue.createdByAgentId)?.icon
|
||||
: undefined,
|
||||
createdAt: issue.createdAt,
|
||||
|
|
|
|||
|
|
@ -2772,6 +2772,8 @@ export function Inbox() {
|
|||
defaultProjectWorkspaceIdByProjectId,
|
||||
})}
|
||||
assigneeName={agentName(issue.assigneeAgentId)}
|
||||
assigneeAgent={agents?.find((agent) => agent.id === issue.assigneeAgentId)}
|
||||
creatorAgent={agents?.find((agent) => agent.id === issue.createdByAgentId)}
|
||||
assigneeUserName={
|
||||
formatAssigneeUserLabel(issue.assigneeUserId, currentUserId, companyUserLabelMap)
|
||||
?? assigneeUserProfile?.label
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useEffect, useRef, useState, useMemo, useCallback } from "react";
|
||||
import { Link, useNavigate } from "@/lib/router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
|
@ -10,7 +11,6 @@ import { Card } from "@/components/ui/card";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { PageSkeleton } from "../components/PageSkeleton";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { Download, Maximize2, Minus, Network, Plus, Upload } from "lucide-react";
|
||||
import { AGENT_ROLE_LABELS, type Agent } from "@paperclipai/shared";
|
||||
import { useCloudInstance } from "@/hooks/useCloudInstance";
|
||||
|
|
@ -598,7 +598,7 @@ export function OrgChart() {
|
|||
{/* Agent icon + status dot */}
|
||||
<div className="relative shrink-0">
|
||||
<div className="w-9 h-9 rounded-full bg-muted flex items-center justify-center">
|
||||
<AgentIcon icon={agent?.icon} className="h-4.5 w-4.5 text-foreground/70" />
|
||||
<AgentAvatar agent={agent} size={16} className="h-4.5 w-4.5 text-foreground/70"/>
|
||||
</div>
|
||||
<span
|
||||
className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-card"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useEffect, useRef, useState, useMemo, useCallback } from "react";
|
||||
import { Link, useNavigate } from "@/lib/router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
|
@ -10,7 +11,6 @@ import { Card } from "@/components/ui/card";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { PageSkeleton } from "../components/PageSkeleton";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { Download, Maximize2, Minus, Network, Plus, Upload } from "lucide-react";
|
||||
import { AGENT_ROLE_LABELS, type Agent } from "@paperclipai/shared";
|
||||
import { useCloudInstance } from "@/hooks/useCloudInstance";
|
||||
|
|
@ -630,7 +630,7 @@ export function OrgChart({ orgTree: providedOrgTree, agents: providedAgents, emb
|
|||
{/* Agent icon + status dot */}
|
||||
<div className="relative shrink-0">
|
||||
<div className="w-9 h-9 rounded-full bg-muted flex items-center justify-center">
|
||||
<AgentIcon icon={agent?.icon} className="h-4.5 w-4.5 text-foreground/70" />
|
||||
<AgentAvatar agent={agent} size={16} className="h-4.5 w-4.5 text-foreground/70"/>
|
||||
</div>
|
||||
<span
|
||||
className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full border-2 border-card"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type FormEvent, type ReactNode } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
|
|
@ -61,7 +62,6 @@ import { PageSkeleton } from "../components/PageSkeleton";
|
|||
import { MarkdownEditor, type MarkdownEditorRef } from "../components/MarkdownEditor";
|
||||
import { RoutineVariablesEditor, RoutineVariablesHint } from "../components/RoutineVariablesEditor";
|
||||
import { PipelineStageHistoryPanel } from "../components/PipelineStageHistoryPanel";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { InlineEntitySelector, type InlineEntityOption } from "../components/InlineEntitySelector";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
|
|
@ -2825,7 +2825,7 @@ export function PipelineSettings() {
|
|||
const agent = option.id.startsWith("agent:") ? agentById.get(option.id.slice("agent:".length)) : null;
|
||||
return (
|
||||
<>
|
||||
{agent ? <AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{agent ? <AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
@ -2835,7 +2835,7 @@ export function PipelineSettings() {
|
|||
const agent = option.id.startsWith("agent:") ? agentById.get(option.id.slice("agent:".length)) : null;
|
||||
return (
|
||||
<>
|
||||
{agent ? <AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{agent ? <AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
@ -2916,7 +2916,7 @@ export function PipelineSettings() {
|
|||
: null;
|
||||
return (
|
||||
<>
|
||||
{agent ? <AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{agent ? <AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
@ -2927,7 +2927,7 @@ export function PipelineSettings() {
|
|||
const agent = agentId ? agentById.get(agentId) : null;
|
||||
return (
|
||||
<>
|
||||
{agent ? <AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{agent ? <AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
@ -3056,7 +3056,7 @@ export function PipelineSettings() {
|
|||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<AgentIcon icon={selectedAutomationAgent.icon} className="h-4 w-4 shrink-0" />
|
||||
<AgentAvatar agent={selectedAutomationAgent} size={16} className="h-4 w-4 shrink-0"/>
|
||||
<span>{selectedAutomationAgent.name} runs this step automatically.</span>
|
||||
</div>
|
||||
<FieldRow label="Issue title">
|
||||
|
|
@ -3143,6 +3143,7 @@ export function PipelineSettings() {
|
|||
hasAutomation={Boolean(detail.routineId && detail.assigneeAgentId)}
|
||||
agentName={automationAgent?.name ?? null}
|
||||
agentIcon={automationAgent?.icon ?? null}
|
||||
agent={automationAgent ?? undefined}
|
||||
secrets={secretsQuery.data ?? []}
|
||||
secretsLoading={secretsQuery.isLoading}
|
||||
value={stageEnv}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useCallback, useEffect, useMemo, useState, type FormEvent, type ReactNode } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { groupWarningsByStage, LOW_TRUST_REVIEW_PRESET } from "@paperclipai/shared";
|
||||
|
|
@ -84,7 +85,6 @@ import { instanceSettingsApi } from "../api/instanceSettings";
|
|||
import { issuesApi } from "../api/issues";
|
||||
import { projectsApi } from "../api/projects";
|
||||
import { EmptyState } from "../components/EmptyState";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { IssueChatThread } from "../components/IssueChatThread";
|
||||
import { MarkdownBody } from "../components/MarkdownBody";
|
||||
import { PageSkeleton } from "../components/PageSkeleton";
|
||||
|
|
@ -1363,7 +1363,7 @@ function PipelineBoardColumn({
|
|||
className="inline-flex max-w-full items-center gap-1 rounded-full border border-border px-2 py-0.5 text-xs font-medium text-muted-foreground hover:text-foreground"
|
||||
title={`Edit ${stage.name} automation`}
|
||||
>
|
||||
<AgentIcon icon={automationAgent.icon} className="h-3.5 w-3.5 shrink-0" />
|
||||
<AgentAvatar agent={automationAgent} size={16} className="h-3.5 w-3.5 shrink-0"/>
|
||||
<span className="truncate">{automationAgent.name}</span>
|
||||
</Link>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { startTransition, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, useNavigate, useSearchParams } from "@/lib/router";
|
||||
|
|
@ -25,7 +26,6 @@ import { EmptyState } from "../components/EmptyState";
|
|||
import { IssuesList } from "../components/IssuesList";
|
||||
import { PageSkeleton } from "../components/PageSkeleton";
|
||||
import { PageTabBar } from "../components/PageTabBar";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { InlineEntitySelector, type InlineEntityOption } from "../components/InlineEntitySelector";
|
||||
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "../components/MarkdownEditor";
|
||||
import { RoutineListRow, nextRoutineStatus } from "../components/RoutineList";
|
||||
|
|
@ -1042,7 +1042,7 @@ export function Routines() {
|
|||
option ? (
|
||||
currentAssignee ? (
|
||||
<>
|
||||
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={currentAssignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -1057,7 +1057,7 @@ export function Routines() {
|
|||
const assignee = agentById.get(option.id);
|
||||
return (
|
||||
<>
|
||||
{assignee ? <AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{assignee ? <AgentAvatar agent={assignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { startTransition, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { Link, Navigate, useNavigate, useSearchParams } from "@/lib/router";
|
||||
|
|
@ -24,7 +25,6 @@ import { EmptyState } from "../components/EmptyState";
|
|||
import { IssuesList } from "../components/IssuesList";
|
||||
import { PageSkeleton } from "../components/PageSkeleton";
|
||||
import { PageTabBar } from "../components/PageTabBar";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
import { InlineEntitySelector, type InlineEntityOption } from "../components/InlineEntitySelector";
|
||||
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "../components/MarkdownEditor";
|
||||
import { RoutineListRow, nextRoutineStatus } from "../components/RoutineList";
|
||||
|
|
@ -1075,7 +1075,7 @@ export function Routines() {
|
|||
option ? (
|
||||
currentAssignee ? (
|
||||
<>
|
||||
<AgentIcon icon={currentAssignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={currentAssignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -1090,7 +1090,7 @@ export function Routines() {
|
|||
const assignee = agentById.get(option.id);
|
||||
return (
|
||||
<>
|
||||
{assignee ? <AgentIcon icon={assignee.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{assignee ? <AgentAvatar agent={assignee} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -368,8 +368,8 @@ export function Search() {
|
|||
placeholderData: (previousData) => previousData,
|
||||
});
|
||||
|
||||
const agentsById = useMemo<ReadonlyMap<string, Pick<Agent, "id" | "name">>>(() => {
|
||||
const map = new Map<string, Pick<Agent, "id" | "name">>();
|
||||
const agentsById = useMemo<ReadonlyMap<string, Pick<Agent, "id" | "name" | "appearance">>>(() => {
|
||||
const map = new Map<string, Pick<Agent, "id" | "name" | "appearance">>();
|
||||
for (const agent of agents) map.set(agent.id, agent);
|
||||
return map;
|
||||
}, [agents]);
|
||||
|
|
@ -774,7 +774,7 @@ interface SearchTabContentProps {
|
|||
sortLabel: string;
|
||||
zeroResultsSlot: ReactNode;
|
||||
isFetching: boolean;
|
||||
agentsById: ReadonlyMap<string, Pick<Agent, "id" | "name">>;
|
||||
agentsById: ReadonlyMap<string, Pick<Agent, "id" | "name" | "appearance">>;
|
||||
}
|
||||
|
||||
function SearchTabContent({
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentIdentity } from "@/components/AgentIdentity";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
|
|
@ -2955,7 +2956,7 @@ function AgentPicker({
|
|||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
{selectedAgent ? (
|
||||
<Identity name={selectedAgent.name} size="xs" />
|
||||
<AgentIdentity agent={selectedAgent} size="xs" />
|
||||
) : (
|
||||
<span className="text-muted-foreground">Pick an agent</span>
|
||||
)}
|
||||
|
|
@ -2988,7 +2989,7 @@ function AgentPicker({
|
|||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
<Identity name={agent.name} size="xs" />
|
||||
<AgentIdentity agent={agent} size="xs" />
|
||||
{!selectable && (
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
Paused
|
||||
|
|
@ -3115,7 +3116,7 @@ function RunDetailView({
|
|||
<div className="min-h-0 flex-1 space-y-3 overflow-auto p-3">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<StatusBadge status={runBadgeStatus(detail.status)} />
|
||||
<Identity name={agentName} size="xs" />
|
||||
<AgentIdentity agent={agent ?? { id: detail.agentId, name: agentName }} size="xs" />
|
||||
{removed && <Badge variant="secondary">removed</Badge>}
|
||||
<span className="font-mono text-xs text-muted-foreground">
|
||||
v{detail.skillVersion.revisionNumber}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useMemo } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { agentsApi } from "@/api/agents";
|
||||
import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import { InlineEntitySelector, type InlineEntityOption } from "@/components/InlineEntitySelector";
|
||||
import { isAgentTaskTarget } from "@/lib/company-members";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
|
|
@ -48,7 +48,7 @@ export function SummarizerAgentSelect({
|
|||
const agent = option.id.startsWith("agent:") ? agentById.get(option.id.slice("agent:".length)) : null;
|
||||
return (
|
||||
<>
|
||||
{agent ? <AgentIcon icon={agent.icon} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" /> : null}
|
||||
{agent ? <AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/> : null}
|
||||
<span className="truncate">{option.label}</span>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Ban, Check, FlaskConical, Loader2, RefreshCw, Search, ShieldQuestion } from "lucide-react";
|
||||
import type { Agent, ToolCatalogEntry, ToolConnectionCapabilities } from "@paperclipai/shared";
|
||||
import { useSearchParams } from "@/lib/router";
|
||||
import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import { AgentMultiSelect } from "@/components/AgentMultiSelect";
|
||||
import { InlineBanner } from "@/components/InlineBanner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -180,7 +180,7 @@ function AgentAccessSection({
|
|||
<div className="space-y-0.5">
|
||||
{selectedAgents.map((agent) => (
|
||||
<div key={agent.id} className="flex items-center gap-2 px-1.5 py-1 text-sm">
|
||||
<AgentIcon icon={agent.icon ?? null} className="h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={agent} size={16} className="h-4 w-4 shrink-0 text-muted-foreground"/>
|
||||
<span className="min-w-0 flex-1 truncate text-foreground">{agent.name}</span>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
import { Download, ScrollText, ShieldAlert } from "lucide-react";
|
||||
|
|
@ -15,7 +16,6 @@ import {
|
|||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Identity } from "@/components/Identity";
|
||||
import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import { cn, relativeTime } from "@/lib/utils";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { formatActivityVerb } from "@/lib/activity-format";
|
||||
|
|
@ -112,7 +112,7 @@ function AuditActor({
|
|||
return (
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5" title={agent.name}>
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<AgentIcon icon={agent.icon} className="h-3 w-3" />
|
||||
<AgentAvatar agent={agent} size={16} className="h-3 w-3"/>
|
||||
</span>
|
||||
<span className="truncate font-medium text-foreground">{agent.name}</span>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
||||
import { Download, ScrollText, ShieldAlert } from "lucide-react";
|
||||
|
|
@ -15,7 +16,6 @@ import {
|
|||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Identity } from "@/components/Identity";
|
||||
import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import { cn, relativeTime } from "@/lib/utils";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { formatActivityVerb } from "@/lib/activity-format";
|
||||
|
|
@ -122,7 +122,7 @@ function AuditActor({
|
|||
return (
|
||||
<span className="inline-flex min-w-0 items-center gap-1.5" title={agent.name}>
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<AgentIcon icon={agent.icon} className="h-3 w-3" />
|
||||
<AgentAvatar agent={agent} size={16} className="h-3 w-3"/>
|
||||
</span>
|
||||
<span className="truncate font-medium text-foreground">{agent.name}</span>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { AgentAvatar } from "@/components/AgentAvatar";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Copy, Fingerprint, KeyRound, Link2, ShieldAlert, Variable, ServerCog } from "lucide-react";
|
||||
|
|
@ -6,7 +7,6 @@ import type {
|
|||
SecretProposalAgentRef,
|
||||
SecretProposalView,
|
||||
} from "@paperclipai/shared";
|
||||
import { AgentIcon } from "@/components/AgentIconPicker";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
|
|
@ -126,7 +126,7 @@ export function AgentRefChip({
|
|||
}) {
|
||||
return (
|
||||
<span className={cn("inline-flex min-w-0 items-center gap-1", className)}>
|
||||
<AgentIcon icon={agent.icon ?? null} className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
|
||||
<AgentAvatar agent={agent} size={16} className="h-3.5 w-3.5 shrink-0 text-muted-foreground"/>
|
||||
<span className="min-w-0 truncate">{agent.name}</span>
|
||||
</span>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
import { useEffect, useState, useRef } from "react";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Route, Routes, useLocation, useNavigate } from "@/lib/router";
|
||||
import { useCompany } from "@/context/CompanyContext";
|
||||
import { PluginLauncherProvider } from "@/plugins/launchers";
|
||||
import { Layout } from "@/components/Layout";
|
||||
import { Agents } from "@/pages/Agents";
|
||||
import { AgentDetail } from "@/pages/AgentDetail";
|
||||
import { IssueDetail } from "@/pages/IssueDetail";
|
||||
import { Dashboard } from "@/pages/Dashboard";
|
||||
import { NewAgent } from "@/pages/NewAgent";
|
||||
import { AgentBasicsDialog } from "@/components/new-agent/AgentBasicsDialog";
|
||||
import { queryKeys } from "@/lib/queryKeys";
|
||||
import { resolveAgentAppearance, agentAvatarUrl } from "@paperclipai/shared";
|
||||
import { storybookAgents, storybookIssues, storybookActivityEvents, storybookLiveRuns, storybookDashboardSummary } from "../fixtures/paperclipData";
|
||||
|
||||
const companyId = "company-storybook";
|
||||
const agents = storybookAgents.map(agent => {
|
||||
const appearance = resolveAgentAppearance(agent.appearance, agent.id);
|
||||
return { ...agent, appearance, avatarUrl: agentAvatarUrl(appearance), chainOfCommand: [], access: { canAssignTasks: true, taskAssignSource: "explicit_grant", membership: null, grants: [] } };
|
||||
});
|
||||
const issue = { ...storybookIssues[0], assigneeAgentId: agents[0].id, status: "in_progress" };
|
||||
|
||||
/** Real route components and navigation; all domain data stays in Storybook. */
|
||||
function installPageFixtures() {
|
||||
const previous = window.fetch;
|
||||
window.fetch = async (input, init) => {
|
||||
const url = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url, location.origin);
|
||||
const path = url.pathname;
|
||||
if (!path.startsWith("/api/") || path.startsWith("/api/agent-avatars/")) return previous(input, init);
|
||||
const json = (data: unknown) => Response.json(data);
|
||||
if (path === "/api/cli-auth/me") return json({ source: "local_implicit", isInstanceAdmin: true, companyIds: [companyId], memberships: [] });
|
||||
if (path === "/api/health") return json({ status: "ok", deploymentMode: "local_trusted", authReady: true, bootstrapStatus: "ready" });
|
||||
if (path === "/api/instance/settings") return json({ experimental: {} });
|
||||
if (path === "/api/instance/settings/experimental") return previous(input, init);
|
||||
if (path.endsWith("/resource-memberships/me")) return json({ projectMemberships: {}, agentMemberships: {}, starredProjectIds: [], starredAgentIds: [], starredDocumentIds: [], projectStarredAt: {}, agentStarredAt: {}, documentStarredAt: {}, updatedAt: null });
|
||||
if (path === `/api/companies/${companyId}/agents`) return json(agents);
|
||||
if (path.endsWith("/dashboard")) return json(storybookDashboardSummary);
|
||||
if (path === `/api/companies/${companyId}/activity`) return json(storybookActivityEvents);
|
||||
if (path === `/api/companies/${companyId}/live-runs`) return json(storybookLiveRuns.map(run => ({ ...run, agentAppearance: agents.find(agent => agent.id === run.agentId)?.appearance })));
|
||||
const agentMatch = path.match(/^\/api\/agents\/([^/]+)(?:\/(.*))?$/);
|
||||
if (agentMatch) {
|
||||
const agent = agents.find(item => item.id === agentMatch[1] || item.urlKey === agentMatch[1]) ?? agents[0];
|
||||
if (!agentMatch[2]) return json(agent);
|
||||
if (agentMatch[2] === "runtime-state") return json({ agentId: agent.id, companyId, adapterType: agent.adapterType, stateJson: {}, sessionId: null, sessionDisplayId: null, totalInputTokens: 42000, totalOutputTokens: 8200, totalCostCents: 340, lastRunStatus: "succeeded" });
|
||||
if (agentMatch[2] === "skills") return json({ desiredSkills: [], actualSkills: [], errors: [] });
|
||||
if (agentMatch[2] === "instructions-bundle") return previous(input, init);
|
||||
return json([]);
|
||||
}
|
||||
const issueMatch = path.match(/^\/api\/issues\/([^/]+)(?:\/(.*))?$/);
|
||||
if (issueMatch) {
|
||||
const item = storybookIssues.find(row => row.id === issueMatch[1] || row.identifier === issueMatch[1]) ?? issue;
|
||||
if (!issueMatch[2]) return json({ ...item, assigneeAgentId: agents[0].id });
|
||||
if (issueMatch[2] === "comments") return json([{ id: "persona-comment", issueId: item.id, companyId, authorAgentId: agents[0].id, authorUserId: null, body: "The implementation is ready for review. I’ve checked the edge cases and added coverage for the new behavior.", createdAt: item.updatedAt, updatedAt: item.updatedAt }]);
|
||||
if (issueMatch[2] === "active-run") return json(null);
|
||||
if (issueMatch[2] === "queued-comments") return json({ queues: [], revision: "storybook", items: [] });
|
||||
if (issueMatch[2] === "cost-summary") return json({ costCents: 340, inputTokens: 42000, outputTokens: 8200, runCount: 1 });
|
||||
if (issueMatch[2].startsWith("documents/")) return new Response(null, { status: 404 });
|
||||
return json([]);
|
||||
}
|
||||
if (path.endsWith("/budgets/overview")) return json({ companyId, policies: [], activeIncidents: [], pausedAgentCount: 0, pausedProjectCount: 0, pendingApprovalCount: 0 });
|
||||
if (path.includes("/heartbeat-runs/") && path.endsWith("/events")) return json([]);
|
||||
// Reuse the preview's auth, company, adapter, issue-list and environment fixtures.
|
||||
if (path === "/api/companies" || path.startsWith("/api/auth/") || path.includes("/adapters") || path.includes("/environments") || path.endsWith("/issues") || path.endsWith("/projects") || path.endsWith("/approvals") || path.endsWith("/sidebar-badges") || path.endsWith("/user-directory")) return previous(input, init);
|
||||
if (path.includes("/settings")) return json({});
|
||||
return json([]);
|
||||
};
|
||||
return () => { window.fetch = previous; };
|
||||
}
|
||||
|
||||
function PersonaPage({ path, meet = false }: { path: string; meet?: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const queryClient = useQueryClient();
|
||||
const { selectedCompanyId, setSelectedCompanyId } = useCompany();
|
||||
const [ready, setReady] = useState(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(meet);
|
||||
const initialPath = useRef<string | null>(null);
|
||||
useEffect(() => installPageFixtures(), []);
|
||||
useEffect(() => {
|
||||
if (initialPath.current === path) return;
|
||||
initialPath.current = path;
|
||||
queryClient.setQueryData(queryKeys.agents.list(companyId), agents);
|
||||
for (const agent of agents) {
|
||||
for (const ref of [agent.id, agent.urlKey]) queryClient.setQueryData([...queryKeys.agents.detail(ref!), companyId], agent);
|
||||
}
|
||||
setSelectedCompanyId(companyId);
|
||||
navigate(path, { replace: true });
|
||||
setReady(true);
|
||||
}, [path, navigate, queryClient, setSelectedCompanyId]);
|
||||
if (!ready || selectedCompanyId !== companyId || location.pathname === "/PAP/storybook") return null;
|
||||
return <PluginLauncherProvider>
|
||||
<Routes>
|
||||
<Route path="/:companyPrefix" element={<Layout />}>
|
||||
<Route path="agents" element={<Agents />} />
|
||||
<Route path="agents/all" element={<Agents />} />
|
||||
<Route path="agents/new" element={<NewAgent />} />
|
||||
<Route path="agents/:agentId/:tab?" element={<AgentDetail />} />
|
||||
<Route path="issues/:issueId" element={<IssueDetail />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
{meet && <AgentBasicsDialog open={dialogOpen} onClose={() => setDialogOpen(false)} onContinue={basics => { setDialogOpen(false); navigate(`/PAP/agents/new?name=${encodeURIComponent(basics.name)}&adapterType=${basics.adapterType}`); }} />}
|
||||
</PluginLauncherProvider>;
|
||||
}
|
||||
const meta = { title: "Agents/Personas/Full pages", parameters: { layout: "fullscreen", a11y: { test: "off" } } } satisfies Meta;
|
||||
export default meta;
|
||||
type Story = StoryObj;
|
||||
export const AllAgents: Story = { render: () => <PersonaPage path="/PAP/agents/all" /> };
|
||||
export const AgentOverview: Story = { render: () => <PersonaPage path="/PAP/agents/codexcoder/overview" /> };
|
||||
export const Task: Story = { render: () => <PersonaPage path={`/PAP/issues/${issue.identifier}`} /> };
|
||||
export const CompanyDashboard: Story = { name: "Dashboard", render: () => <PersonaPage path="/PAP/dashboard" /> };
|
||||
export const MeetYourNextAgent: Story = { render: () => <PersonaPage path="/PAP/agents/all" meet /> };
|
||||
export const NewAgentConnection: Story = { render: () => <PersonaPage path="/PAP/agents/new?name=Carl&adapterType=codex_local" /> };
|
||||
Loading…
Reference in New Issue