Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type RoomAvatarProps = {
ariaLabel?: string;
count?: number;
members: RoomAvatarMember[];
size?: 32 | 48;
size?: number;
showCountBadge?: boolean;
};

Expand Down
24 changes: 24 additions & 0 deletions web/app/src/models/tasks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { AgentLike } from "@/models/agents";

export type WorkspaceTask = {
id: string;
assignment_type: string;
Expand Down Expand Up @@ -303,6 +305,28 @@ export function displayTeam(team: WorkspaceTeam): string {
return team.title || team.id;
}

export function teamMemberIDs(team: WorkspaceTeam): string[] {
const ids = new Set(team.member_agent_ids ?? []);
if (team.lead_agent_id) {
const altID = team.lead_agent_id.startsWith("u-") ? team.lead_agent_id.slice(2) : "u-" + team.lead_agent_id;
ids.delete(altID);
ids.add(team.lead_agent_id);
}
return Array.from(ids);
}

export function resolveTeamAvatarMembers(team: WorkspaceTeam, agents: readonly AgentLike[]) {
return teamMemberIDs(team).map((memberID) => {
const agent = agents.find((item) => item.id === memberID);
return {
id: memberID,
name: agent?.name || memberID,
avatar: agent?.avatar || "",
accent_hex: undefined,
};
});
}

type TranslateFn = (key: string) => string;

export function taskStatusLabel(status: unknown, t: TranslateFn): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: var(--radius-xl);
background: color-mix(in srgb, var(--blue-600) 10%, transparent);
color: var(--blue-600);
border: 0;
border-radius: 0;
background: transparent;
color: inherit;
}

.teamDetailAvatar svg {
Expand Down Expand Up @@ -132,9 +133,9 @@
justify-content: center;
width: 76px;
height: 76px;
border-radius: 14px;
background: var(--panel-soft);
color: var(--muted);
border-radius: 0;
background: transparent;
color: inherit;
}

.teamProfileBlock h2,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { useState } from "react";
import type { ReactNode } from "react";
import { AgentAvatarContent, RoomAvatar } from "@/components/business";
import { ListChecks, Trash2, UserPlus, Users } from "lucide-react";
import { TaskSubtaskIndicator } from "@/components/business";
import { Button } from "@/components/ui";
import { AgentIcon, UsersIcon } from "@/components/ui/Icons";
import { isAgentRunning } from "@/models/agents";
import type { AgentLike } from "@/models/agents";
import type { IMUser, TranslateFn, UsersById } from "@/models/conversations";
import {
displayTeam,
formatTaskUpdatedAt,
resolveTeamAvatarMembers,
resolveTaskSidebarPhase,
rootTasks,
taskChildren,
teamMemberIDs,
teamStatusLabel,
} from "@/models/tasks";
import type { WorkspaceTask, WorkspaceTeam } from "@/models/tasks";
import { avatarFallbackText } from "@/shared/avatar";
import { classNames } from "@/shared/lib/classNames";
import styles from "./TeamDetailPane.module.css";

Expand Down Expand Up @@ -67,14 +70,15 @@ export function TeamDetailPane({

const memberIDs = teamMemberIDs(team);
const members = memberIDs.map((memberID) => memberDisplay(memberID, agents, usersById, team.lead_agent_id));
const avatarMembers = resolveTeamAvatarMembers(team, agents);
const parentTasks = rootTasks(tasks);
const locale = document.documentElement.lang || "en";

return (
<section className={classNames("entity-pane", "team-detail-pane", styles.teamDetailPane)}>
<header className={classNames("entity-header", styles.contentWidth, styles.teamDetailHeader)}>
<div className={classNames("entity-avatar", styles.teamDetailAvatar)}>
<UsersIcon />
<RoomAvatar ariaLabel={displayTeam(team)} count={memberIDs.length} members={avatarMembers} size={48} />
</div>
<div className={classNames("entity-heading", styles.teamDetailHeading)}>
<div className={classNames("entity-title-row", styles.teamDetailTitleRow)}>
Expand Down Expand Up @@ -121,7 +125,7 @@ export function TeamDetailPane({
<aside className={classNames(styles.panelSurface, styles.teamDetailSummary)}>
<div className={styles.teamProfileBlock}>
<div className={styles.teamProfileIcon}>
<Users size={34} aria-hidden="true" />
<RoomAvatar ariaLabel={displayTeam(team)} count={memberIDs.length} members={avatarMembers} size={76} />
</div>
<div>
<h2>{displayTeam(team)}</h2>
Expand Down Expand Up @@ -289,7 +293,14 @@ function MemberRowContent({ member, t }: { member: TeamMemberDisplay; t: Transla
return (
<>
<span className={classNames(styles.teamMemberAvatar, member.agent && styles.agent)}>
{member.agent ? <AgentIcon /> : member.initials}
{member.agent ? (
<AgentAvatarContent
avatar={member.agent.avatar}
fallback={avatarFallbackText(member.agent.avatar, member.agent.name, member.agent.id)}
/>
) : (
member.initials
)}
</span>
<span className={classNames(styles.rowMain, styles.teamMemberMain)}>
<span className={styles.teamMemberTitleLine}>
Expand All @@ -305,18 +316,6 @@ function MemberRowContent({ member, t }: { member: TeamMemberDisplay; t: Transla
);
}

function teamMemberIDs(team: WorkspaceTeam): string[] {
const ids = new Set(team.member_agent_ids ?? []);
if (team.lead_agent_id) {
// The lead agent ID (e.g., "u-manager") may differ from the room participant ID
// (e.g., "manager"). Remove the participant ID form to avoid double-counting.
const altID = team.lead_agent_id.startsWith("u-") ? team.lead_agent_id.slice(2) : "u-" + team.lead_agent_id;
ids.delete(altID);
ids.add(team.lead_agent_id);
}
return Array.from(ids);
}

function memberDisplay(
memberID: string,
agents: readonly AgentLike[],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,13 +420,27 @@
}

.agentRow {
padding: 12px;
grid-template-columns: 32px minmax(0, 1fr) max-content;
}

.humanRow {
padding: 12px;
}

.computerRow {
padding: 12px;
}

.conversationRow {
padding: 12px;
grid-template-columns: 32px minmax(0, 1fr) auto;
}

.directConversationRow {
align-items: center;
}

.taskRow {
grid-template-columns: minmax(0, 1fr) max-content;
align-items: center;
Expand All @@ -442,6 +456,34 @@
color: var(--gray-100);
}

:global(:root[data-theme="dark"]) .conversationRow.active,
:global(:root[data-theme="dark"]) .active.conversationRow {
background: var(--gray-800);
color: var(--white);
box-shadow: none;
}

:global(:root[data-theme="dark"]) .agentRow.active,
:global(:root[data-theme="dark"]) .active.agentRow {
background: var(--gray-800);
color: var(--white);
box-shadow: none;
}

:global(:root[data-theme="dark"]) .humanRow.active,
:global(:root[data-theme="dark"]) .active.humanRow {
background: var(--gray-800);
color: var(--white);
box-shadow: none;
}

:global(:root[data-theme="dark"]) .computerRow.active,
:global(:root[data-theme="dark"]) .active.computerRow {
background: var(--gray-800);
color: var(--white);
box-shadow: none;
}

:global(:root[data-theme="dark"]) .groupToggle,
:global(:root[data-theme="dark"]) .groupTitle,
:global(:root[data-theme="dark"]) .groupTitle span {
Expand Down Expand Up @@ -479,6 +521,19 @@
color: var(--gray-500);
}

:global(:root[data-theme="dark"]) .conversationRow.active .meta,
:global(:root[data-theme="dark"]) .active.conversationRow .meta,
:global(:root[data-theme="dark"]) .agentRow.active .meta,
:global(:root[data-theme="dark"]) .active.agentRow .meta,
:global(:root[data-theme="dark"]) .humanRow.active .meta,
:global(:root[data-theme="dark"]) .active.humanRow .meta,
:global(:root[data-theme="dark"]) .computerRow.active .meta,
:global(:root[data-theme="dark"]) .active.computerRow .meta,
:global(:root[data-theme="dark"]) .conversationRow.active .time,
:global(:root[data-theme="dark"]) .active.conversationRow .time {
color: var(--white);
}

:global(:root[data-theme="dark"]) .addButton:hover:not(:disabled),
:global(:root[data-theme="dark"]) .addButton:focus-visible {
border-color: color-mix(in srgb, var(--gray-300) 28%, transparent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,10 +419,10 @@ export function WorkspaceConversationRow({
: avatarFallbackText(displayUser?.avatar, displayUser?.name, displayUser?.id);
const title = isDirect && displayUser ? displayUser.name : conversation.title;
const roomAvatarMembers = resolveRoomAvatarMembers(conversation, usersById, currentUserID);
const preview = formatConversationPreview(lastMessage, conversation, currentUserID, usersById, locale, t);
const preview = isDirect ? "" : formatConversationPreview(lastMessage, conversation, currentUserID, usersById, locale, t);
return (
<button
className={classNames(styles.row, styles.conversationRow, active && styles.active)}
className={classNames(styles.row, styles.conversationRow, isDirect && styles.directConversationRow, active && styles.active)}
onClick={() => onSelect(conversation.id)}
>
<span
Expand Down Expand Up @@ -466,9 +466,11 @@ export function WorkspaceConversationRow({
></span>
) : null}
</span>
<span className={classNames(styles.meta, "truncate")}>
<MessagePreviewText content={preview} />
</span>
{isDirect ? null : (
<span className={classNames(styles.meta, "truncate")}>
<MessagePreviewText content={preview} />
</span>
)}
</span>
<span className={styles.time}>{formatTime(lastMessage?.created_at, locale)}</span>
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
text-decoration: none;
}

.teamRow {
padding: 12px;
}

.modelProviderIconShell {
background: var(--white);
}
Expand Down Expand Up @@ -99,3 +103,13 @@
:global(:root[data-theme="dark"]) .modelProviderIconShell {
background: var(--white);
}

:global(:root[data-theme="dark"]) .teamRow:global(.active) {
background: var(--gray-800);
color: var(--white);
box-shadow: none;
}

:global(:root[data-theme="dark"]) .teamRow:global(.active) * {
color: var(--white);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import type { DragEvent } from "react";
import { FileCode2, Plus } from "lucide-react";
import { ModelsIcon, UsersIcon } from "@/components/ui/Icons";
import { RoomAvatar } from "@/components/business";
import { ModelsIcon } from "@/components/ui/Icons";
import { isDirectConversation, resolveConversationUser } from "@/models/conversations";
import { modelProviderAvatarPath, providerStatusTone, type ModelProvider } from "@/models/modelProviders";
import { WorkspacePaneTypes, WorkspaceTabs } from "@/models/routing";
import { displayTeam } from "@/models/tasks";
import { displayTeam, resolveTeamAvatarMembers, teamMemberIDs } from "@/models/tasks";
import { localizeTemplateSourceTag } from "@/shared/i18n";
import { classNames } from "@/shared/lib/classNames";
import { WORKSPACE_SECTION_ORDER_STORAGE_KEY } from "@/shared/storage/keys";
Expand Down Expand Up @@ -728,7 +729,9 @@ export function WorkspaceTabPanels({
>
{visibleTeams.length ? (
visibleTeams.map((team) => {
const memberCount = team.member_agent_ids.length + (team.lead_agent_id ? 1 : 0);
const memberIDs = teamMemberIDs(team);
const memberCount = memberIDs.length;
const avatarMembers = resolveTeamAvatarMembers(team, agentItems);
return (
<button
key={team.id}
Expand All @@ -740,7 +743,7 @@ export function WorkspaceTabPanels({
onClick={() => onSelectTeam?.(team)}
>
<span className={rowStyles.icon}>
<UsersIcon />
<RoomAvatar ariaLabel={displayTeam(team)} count={memberCount} members={avatarMembers} />
</span>
<span className={rowStyles.main}>
<span className={classNames(rowStyles.title, "truncate")}>{displayTeam(team)}</span>
Expand Down
Loading