Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
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 @@ -1860,6 +1860,53 @@ export const WithPRStateIcons: Story = {
routing: agentsRouting,
}),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await waitFor(() => {
expect(canvas.getByLabelText("Pull request open")).toBeInTheDocument();
expect(canvas.getByLabelText("Draft pull request")).toBeInTheDocument();
expect(canvas.getByLabelText("Pull request merged")).toBeInTheDocument();
expect(canvas.getByLabelText("Pull request closed")).toBeInTheDocument();
});
},
};

export const ActiveChatKebabPersistent: Story = {
args: {
chats: [
buildChat({
id: "active-chat",
title: "Active chat",
updated_at: recentTimestamp,
}),
buildChat({
id: "other-chat",
title: "Other chat",
updated_at: recentTimestamp,
}),
],
},
parameters: {
reactRouter: reactRouterParameters({
location: {
path: "/agents/active-chat",
pathParams: { agentId: "active-chat" },
},
routing: agentsRouting,
}),
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const activeTrigger = await canvas.findByLabelText(
"Open actions for Active chat",
);
// The active chat keeps its actions trigger visible without hover.
await waitFor(() => {
expect(window.getComputedStyle(activeTrigger).opacity).toBe("1");
});
const otherTrigger = canvas.getByLabelText("Open actions for Other chat");
expect(window.getComputedStyle(otherTrigger).opacity).toBe("0");
},
};

export const WithUnreadChats: Story = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,8 +255,11 @@ const ChatSearchResultRow: FC<ChatSearchResultRowProps> = ({
const {
icon: StatusIcon,
className: statusClassName,
label: statusLabel,
prIcon,
diffStatus,
} = getChatDisplayConfig(chat);
const PRIcon = prIcon?.icon;
const additions = diffStatus?.additions ?? 0;
const deletions = diffStatus?.deletions ?? 0;
const changedFiles = diffStatus?.changed_files ?? 0;
Expand Down Expand Up @@ -285,12 +288,23 @@ const ChatSearchResultRow: FC<ChatSearchResultRowProps> = ({
isSelected && "bg-surface-tertiary/40 text-content-primary",
)}
>
<StatusIcon className={cn("mt-1 size-3.5 shrink-0", statusClassName)} />
<StatusIcon
role="img"
aria-label={statusLabel}
className={cn("mt-1 size-3.5 shrink-0", statusClassName)}
/>
<div className="min-w-0">
<div className="truncate text-sm text-content-primary">
{chat.title}
</div>
<div className="flex min-w-0 items-center gap-1.5 text-xs">
{PRIcon && prIcon && (
<PRIcon
role="img"
aria-label={prIcon.label}
className={cn("size-3.5 shrink-0", prIcon.className)}
/>
)}
{hasLineStats && (
<span className="inline-flex shrink-0 items-center gap-0.5 tabular-nums">
<span className="text-git-added-bright">+{additions}</span>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
BotIcon,
ChevronDownIcon,
ChevronRightIcon,
EllipsisVerticalIcon,
Expand Down Expand Up @@ -127,8 +126,11 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
const {
icon: StatusIcon,
className: statusClassName,
label: statusLabel,
prIcon,
diffStatus,
} = getChatDisplayConfig(chat);
const PRIcon = prIcon?.icon;
const hasLinkedDiffStatus = Boolean(diffStatus?.url);
const changedFiles = diffStatus?.changed_files ?? 0;
const additions = diffStatus?.additions ?? 0;
Expand Down Expand Up @@ -199,6 +201,8 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
? `agents-tree-executing-${chat.id}`
: undefined
}
role="img"
aria-label={statusLabel}
className={cn("size-3.5 shrink-0", statusClassName)}
/>
</div>
Expand Down Expand Up @@ -242,16 +246,12 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
)}
</div>
<div className="flex min-w-0 items-center gap-1.5">
{hasChildren && (
<span
className="inline-flex shrink-0 items-center gap-0.5 text-[13px] leading-4 tabular-nums text-content-secondary"
title={`${childIDs.length} ${
childIDs.length === 1 ? "subagent" : "subagents"
}`}
>
{childIDs.length}
<BotIcon className="size-3.5" aria-hidden="true" />
</span>
{PRIcon && prIcon && (
<PRIcon
role="img"
aria-label={prIcon.label}
className={cn("size-3.5 shrink-0", prIcon.className)}
/>
)}
{hasLinkedDiffStatus && hasLineStats && (
<span
Expand Down Expand Up @@ -297,14 +297,17 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
// keep the timestamp visible.
hasMenuActions &&
"[@media(hover:hover)]:group-hover:hidden group-has-[[data-state=open]]:hidden",
hasMenuActions && isActiveChat && "hidden",
)}
>
{chat.has_unread && !isActiveChat ? (
<span
className="size-2 shrink-0 rounded-full bg-content-link pr-1"
data-testid={`unread-indicator-${chat.id}`}
aria-hidden="true"
/>
<span className="flex w-3.5 shrink-0 justify-center">
<span
className="size-2 rounded-full bg-content-link"
data-testid={`unread-indicator-${chat.id}`}
aria-hidden="true"
/>
</span>
) : (
<>
{/* Pin the ignored mask width so Pixel does not diff bounding rect changes. */}
Expand All @@ -331,7 +334,10 @@ export const ChatTreeNode: FC<ChatTreeNodeProps> = ({ chat, isChildNode }) => {
<Button
size="icon"
variant="subtle"
className="absolute inset-0 flex h-6 w-7 min-w-0 justify-end rounded-none px-0 opacity-0 text-content-secondary hover:text-content-primary [@media(hover:hover)]:group-hover:opacity-100 data-[state=open]:opacity-100"
className={cn(
"absolute inset-0 flex h-6 w-7 min-w-0 justify-end rounded-none px-0 opacity-0 text-content-secondary hover:text-content-primary [@media(hover:hover)]:group-hover:opacity-100 data-[state=open]:opacity-100",
isActiveChat && "opacity-100",
)}
aria-label={`Open actions for ${chat.title}`}
>
<EllipsisVerticalIcon className="size-3.5" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,36 @@ import type { Chat, ChatDiffStatus, ChatStatus } from "#/api/typesGenerated";
type ChatIconConfig = {
icon: LucideIcon;
className: string;
label: string;
};

const statusConfig = {
waiting: { icon: CheckIcon, className: "text-content-secondary" },
running: { icon: Loader2Icon, className: "text-content-link animate-spin" },
interrupting: { icon: PauseIcon, className: "text-content-warning" },
requires_action: { icon: PauseIcon, className: "text-content-warning" },
error: { icon: AlertTriangleIcon, className: "text-content-destructive" },
} as const;
waiting: {
icon: CheckIcon,
className: "text-content-secondary",
label: "Idle",
},
running: {
icon: Loader2Icon,
className: "text-content-link animate-spin",
label: "Working",
},
interrupting: {
icon: PauseIcon,
className: "text-content-warning",
label: "Interrupting",
},
requires_action: {
icon: PauseIcon,
className: "text-content-warning",
label: "Requires action",
},
error: {
icon: AlertTriangleIcon,
className: "text-content-destructive",
label: "Error",
},
} as const satisfies Record<ChatStatus, ChatIconConfig>;

const getStatusConfig = (status: ChatStatus): ChatIconConfig => {
return statusConfig[status] ?? statusConfig.waiting;
Expand All @@ -36,50 +57,58 @@ const getPRIconConfig = (
return undefined;
}
if (state === "merged") {
return { icon: GitMergeIcon, className: "text-git-merged-bright" };
return {
icon: GitMergeIcon,
className: "text-git-merged-bright",
label: "Pull request merged",
};
}
if (state === "closed") {
return {
icon: GitPullRequestClosedIcon,
className: "text-git-deleted-bright",
label: "Pull request closed",
};
}
if (diffStatus?.pull_request_draft) {
return {
icon: GitPullRequestDraftIcon,
className: "text-content-secondary",
label: "Draft pull request",
};
}
return { icon: GitPullRequestArrowIcon, className: "text-git-added-bright" };
return {
icon: GitPullRequestArrowIcon,
className: "text-git-added-bright",
label: "Pull request open",
};
};

const getChatDiffStatus = (chat: Chat): ChatDiffStatus | undefined => {
return chat.diff_status;
};

/**
* Returns the icon and styling that represents a chat's current state.
* Returns the icons and styling that represent a chat's current state.
*
* Combines `getStatusConfig` and `getPRIconConfig`: when the chat is in the
* settled `waiting` state and has a linked PR, the PR icon takes precedence
* so list rows surface the merge / closed / draft state instead of the
* generic status icon.
* The status icon always reflects the chat status.
*/
export const getChatDisplayConfig = (
chat: Chat,
): {
icon: LucideIcon;
className: string;
label: string;
prIcon: ChatIconConfig | undefined;
diffStatus: ChatDiffStatus | undefined;
} => {
const diffStatus = getChatDiffStatus(chat);
const baseConfig = getStatusConfig(chat.status);
const prConfig =
chat.status === "waiting" ? getPRIconConfig(diffStatus) : undefined;
const config = prConfig ?? baseConfig;
const config = getStatusConfig(chat.status);
return {
icon: config.icon,
className: config.className,
label: config.label,
prIcon: getPRIconConfig(diffStatus),
diffStatus,
};
};
Loading