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
53 changes: 1 addition & 52 deletions web/app/src/hooks/workspace/useAgentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ import { skillDescriptionFromMarkdown, skillOptionsFromWorkspace } from "@/model
import { useCLIProxyAuthStatuses } from "./useCLIProxyAuthStatuses";
import { workspaceQueryKeys } from "./workspaceQueries";
import type { MessageAction, MessageActionError, MessageLike } from "@/components/business/MessageContent/types";
import type { IMConversation, IMUser, TranslateFn } from "@/models/conversations";
import type { IMConversation, IMUser } from "@/models/conversations";
import type { UseAgentControllerArgs } from "./types";

type ManagerRebuildOptions = {
Expand Down Expand Up @@ -1446,9 +1446,6 @@ export function useAgentController({
openManagerRebuildModal(item);
return;
}
if (action === "delete" && !window.confirm(agentDeleteConfirmationMessage(item, t))) {
return;
}
setAgentActionBusy(`${item.id}:${action}`);
clearAgentOperationError(item);
try {
Expand Down Expand Up @@ -2036,51 +2033,3 @@ function csgclawParticipantIDForAgent(item: AgentLike): string {
);
return String(participant?.id || item.id || "").trim();
}

function agentDeleteConfirmationMessage(item: AgentLike, t: TranslateFn): string {
const name = String(item.name || item.id || "").trim();
const message = t("agentDeleteConfirmMessage", { name });
const channels = agentDeleteBoundChannels(item);
if (channels.length === 0) {
return message;
}
return [
message,
"",
t("agentDeleteBoundChannels", { channels: channels.join(", ") }),
"",
t("agentDeleteCascadeNote"),
].join("\n");
}

function agentDeleteBoundChannels(item: AgentLike): string[] {
const agentID = String(item.id || "").trim();
const channels = new Set<string>();
for (const participant of item.participants || []) {
const participantID = String(participant?.id || "").trim();
if (!participantID) {
continue;
}
const participantAgentID = String(participant?.agent_id || "").trim();
if (participantAgentID && agentID && participantAgentID !== agentID) {
continue;
}
const channel = String(participant?.channel || "")
.trim()
.toLowerCase();
if (!channel || channel === "csgclaw") {
continue;
}
channels.add(agentDeleteChannelLabel(channel));
}
return Array.from(channels).sort((left, right) => left.localeCompare(right));
}

function agentDeleteChannelLabel(channel: string): string {
if (channel === "feishu") {
return "Feishu";
}
return channel.replace(/(^|[-_\s]+)(\w)/g, (_, separator: string, value: string) => {
return `${separator ? " " : ""}${value.toUpperCase()}`;
});
}
18 changes: 18 additions & 0 deletions web/app/src/models/agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,24 @@ export function hasConnectedAgentChannel(item: AgentLike | null | undefined, cha
return agentConnectedChannels(item).some((channel) => channel.id === channelID);
}

type TranslateFnWithParams = (key: string, params?: Record<string, string | number>) => string;

export function agentDeleteConfirmationMessage(item: AgentLike | null | undefined, t: TranslateFnWithParams): string {
const name = String(item?.name || item?.id || "").trim();
const message = t("agentDeleteConfirmMessage", { name });
const channels = agentConnectedChannels(item).map((channel) => channel.name);
if (channels.length === 0) {
return message;
}
return [
message,
"",
t("agentDeleteBoundChannels", { channels: channels.join(", ") }),
"",
t("agentDeleteCascadeNote"),
].join("\n");
}

export function normalizeNotifierDeliveryMode(mode: unknown): string {
const value = String(mode || "")
.trim()
Expand Down
177 changes: 177 additions & 0 deletions web/app/src/pages/AgentPage/components/AgentView/AgentView.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
.csg-dialog-overlay.agent-delete-backdrop {
background: rgba(16, 24, 40, 0.72);
}

.csg-dialog-content.agent-delete-dialog {
width: min(400px, calc(100vw - 32px));
overflow: hidden;
border: 0;
border-radius: 12px;
background: var(--white);
color: var(--gray-900);
box-shadow:
0 20px 24px -4px rgba(16, 24, 40, 0.08),
0 8px 8px -4px rgba(16, 24, 40, 0.03);
}

.agent-delete-header {
position: relative;
display: block;
padding: 24px 72px 0 24px;
border: 0;
background: transparent;
}

.agent-delete-copy {
display: grid;
gap: 4px;
min-width: 0;
}

.agent-delete-dialog .csg-dialog-title {
color: var(--gray-900);
font-size: 18px;
font-weight: 500;
line-height: 28px;
}

.agent-delete-description.csg-dialog-description {
margin-top: 0;
color: var(--gray-600);
font-size: 14px;
line-height: 22px;
white-space: pre-line;
}

.agent-delete-dialog .csg-dialog-close {
--btn-height: 44px;
--btn-min-width: 44px;

position: absolute;
top: 16px;
right: 16px;
width: 44px;
min-width: 44px;
height: 44px;
min-height: 44px;
border-radius: 8px;
color: var(--gray-400);
box-shadow: none;
}

.agent-delete-dialog .csg-dialog-close .icon-button-mark {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
line-height: 0;
}

.agent-delete-dialog .csg-dialog-close svg {
display: block;
flex: 0 0 auto;
width: 24px;
height: 24px;
}

.agent-delete-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
padding: 32px 24px 24px;
}

.agent-delete-actions .agent-delete-button {
width: 100%;
min-width: 0;
height: 44px;
min-height: 44px;
border-radius: 8px;
font-size: 16px;
line-height: 24px;
}

.csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-secondary-gray {
border-color: var(--gray-300);
background: var(--white);
color: var(--gray-700);
box-shadow: var(--shadow-xs);
}

.csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-secondary-gray:hover:not(:disabled),
.csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-secondary-gray:focus-visible {
border-color: var(--gray-300);
background: var(--gray-50);
color: var(--gray-800);
}

.csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-danger {
border-color: var(--error-600);
background: var(--error-600);
color: var(--white);
box-shadow: var(--shadow-xs);
}

.csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-danger:hover:not(:disabled),
.csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-danger:focus-visible {
border-color: var(--error-700);
background: var(--error-700);
color: var(--white);
}

:root[data-theme="dark"] .csg-dialog-overlay.agent-delete-backdrop {
background: rgba(2, 6, 23, 0.72);
}

:root[data-theme="dark"] .csg-dialog-content.agent-delete-dialog {
border-color: color-mix(in oklab, var(--line) 75%, transparent);
background: color-mix(in oklab, var(--panel) 94%, #020617 6%);
color: var(--text);
box-shadow:
0 24px 36px -12px rgba(2, 6, 23, 0.6),
0 10px 18px -10px rgba(2, 6, 23, 0.38);
}

:root[data-theme="dark"] .agent-delete-dialog .csg-dialog-title {
color: var(--text);
}

:root[data-theme="dark"] .agent-delete-description.csg-dialog-description {
color: var(--muted);
}

:root[data-theme="dark"] .agent-delete-dialog .csg-dialog-close {
color: var(--muted);
}

:root[data-theme="dark"] .agent-delete-dialog .csg-dialog-close:hover:not(:disabled),
:root[data-theme="dark"] .agent-delete-dialog .csg-dialog-close:focus-visible {
color: var(--text);
}

:root[data-theme="dark"] .csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-secondary-gray {
border-color: color-mix(in oklab, var(--line) 80%, transparent);
background: color-mix(in oklab, var(--panel) 88%, #020617 12%);
color: var(--text);
}

:root[data-theme="dark"] .csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-secondary-gray:hover:not(:disabled),
:root[data-theme="dark"] .csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-secondary-gray:focus-visible {
border-color: color-mix(in oklab, var(--line) 80%, transparent);
background: color-mix(in oklab, var(--panel) 80%, #020617 20%);
color: var(--text);
}

:root[data-theme="dark"] .csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-danger {
border-color: var(--error-600);
background: color-mix(in oklab, var(--error-600) 88%, #020617 12%);
color: var(--white);
}

:root[data-theme="dark"] .csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-danger:hover:not(:disabled),
:root[data-theme="dark"] .csg-dialog-content.agent-delete-dialog .agent-delete-actions .btn-danger:focus-visible {
border-color: var(--error-700);
background: color-mix(in oklab, var(--error-600) 78%, #020617 22%);
color: var(--white);
}
80 changes: 76 additions & 4 deletions web/app/src/pages/AgentPage/components/AgentView/AgentView.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,83 @@
import { isNotificationBotAgent } from "@/models/agents";
import { useEffect, useState } from "react";
import {
Button,
DialogCloseButton,
DialogContent,
DialogDescription,
DialogHeader,
DialogRoot,
DialogTitle,
} from "@/components/ui";
import { isNotificationBotAgent, agentDeleteConfirmationMessage } from "@/models/agents";
import type { AgentLike } from "@/models/agents";
import { AgentDetailPane } from "../AgentDetailPane";
import type { AgentDetailPaneProps } from "../AgentDetailPane";
import { NotificationParticipantDetailPane } from "../NotificationParticipantDetailPane";

export function AgentView(props: AgentDetailPaneProps) {
if (isNotificationBotAgent(props.item)) {
return <NotificationParticipantDetailPane {...props} />;
const [deletePendingAgent, setDeletePendingAgent] = useState<AgentLike | null>(null);
const deleteConfirmMessage = deletePendingAgent ? agentDeleteConfirmationMessage(deletePendingAgent, props.t) : "";

useEffect(() => {
setDeletePendingAgent(null);
}, [props.item?.id]);

function requestDelete(item: AgentLike) {
setDeletePendingAgent(item);
}

async function confirmDelete() {
const item = deletePendingAgent;
if (!item) {
return;
}
setDeletePendingAgent(null);
await Promise.resolve(props.onDelete(item));
}
return <AgentDetailPane {...props} />;

const sharedProps = {
...props,
onDelete: requestDelete,
};

return (
<>
{isNotificationBotAgent(props.item) ? (
<NotificationParticipantDetailPane {...sharedProps} />
) : (
<AgentDetailPane {...sharedProps} />
)}
<DialogRoot
open={Boolean(deletePendingAgent)}
onOpenChange={(open) => {
if (!open) {
setDeletePendingAgent(null);
}
}}
>
<DialogContent className="agent-delete-dialog" overlayClassName="agent-delete-backdrop">
<DialogHeader className="agent-delete-header">
<div className="agent-delete-copy">
<DialogTitle>{props.t("agentDelete")}</DialogTitle>
<DialogDescription className="agent-delete-description">{deleteConfirmMessage}</DialogDescription>
</div>
<DialogCloseButton label={props.t("close")} size="sm" variant="tertiaryGray" />
</DialogHeader>
<div className="agent-delete-actions">
<Button
className="agent-delete-button"
variant="secondaryGray"
size="sm"
onClick={() => setDeletePendingAgent(null)}
>
{props.t("cancel")}
</Button>
<Button className="agent-delete-button" variant="danger" size="sm" onClick={confirmDelete}>
{props.t("agentDelete")}
</Button>
</div>
</DialogContent>
</DialogRoot>
</>
);
}
2 changes: 2 additions & 0 deletions web/app/src/pages/AgentPage/components/AgentView/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
import "./AgentView.css";

export * from "./AgentView";
Loading
Loading