Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22afdbf2e8 | |||
| ed9828befe | |||
| 968d798a2a | |||
| 7da0ed0e39 | |||
| 166b45e529 |
@@ -27,32 +27,6 @@ import VerifiedUserRounded from "@mui/icons-material/VerifiedUserRounded";
|
|||||||
import type { PermissionReply } from "@/lib/chatStream";
|
import type { PermissionReply } from "@/lib/chatStream";
|
||||||
import type { Message } from "./GlobalChatbox.types";
|
import type { Message } from "./GlobalChatbox.types";
|
||||||
|
|
||||||
const formatMetadataValue = (value: unknown) => {
|
|
||||||
if (typeof value === "string") {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return JSON.stringify(value);
|
|
||||||
} catch {
|
|
||||||
return "[unserializable]";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const truncateText = (value: string, maxLength: number) =>
|
|
||||||
value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
|
|
||||||
|
|
||||||
const formatMetadata = (metadata: Record<string, unknown>) => {
|
|
||||||
const entries = Object.entries(metadata)
|
|
||||||
.filter(([key]) => !["command", "path", "file", "directory"].includes(key))
|
|
||||||
.slice(0, 3);
|
|
||||||
if (!entries.length) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return entries
|
|
||||||
.map(([key, value]) => `${key}: ${truncateText(formatMetadataValue(value), 64)}`)
|
|
||||||
.join(";");
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPermissionTitle = (permission: NonNullable<Message["permissions"]>[number]) => {
|
const getPermissionTitle = (permission: NonNullable<Message["permissions"]>[number]) => {
|
||||||
if (permission.permission === "external_directory") return "访问工作区外目录";
|
if (permission.permission === "external_directory") return "访问工作区外目录";
|
||||||
if (permission.permission === "bash") return "执行终端命令";
|
if (permission.permission === "bash") return "执行终端命令";
|
||||||
@@ -63,15 +37,8 @@ const getPermissionTitle = (permission: NonNullable<Message["permissions"]>[numb
|
|||||||
const getPermissionPrimaryValue = (
|
const getPermissionPrimaryValue = (
|
||||||
permission: NonNullable<Message["permissions"]>[number],
|
permission: NonNullable<Message["permissions"]>[number],
|
||||||
) => {
|
) => {
|
||||||
const command = permission.metadata.command;
|
if (typeof permission.target === "string" && permission.target.trim()) {
|
||||||
if (typeof command === "string" && command.trim()) {
|
return permission.target.trim();
|
||||||
return command.trim();
|
|
||||||
}
|
|
||||||
for (const key of ["path", "file", "directory"]) {
|
|
||||||
const value = permission.metadata[key];
|
|
||||||
if (typeof value === "string" && value.trim()) {
|
|
||||||
return value.trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return permission.patterns[0] ?? permission.permission;
|
return permission.patterns[0] ?? permission.permission;
|
||||||
};
|
};
|
||||||
@@ -94,6 +61,7 @@ const getPermissionStatusLabel = (status: NonNullable<Message["permissions"]>[nu
|
|||||||
if (status === "approved_always") return "已始终允许";
|
if (status === "approved_always") return "已始终允许";
|
||||||
if (status === "approved_once") return "已允许一次";
|
if (status === "approved_once") return "已允许一次";
|
||||||
if (status === "rejected") return "已拒绝";
|
if (status === "rejected") return "已拒绝";
|
||||||
|
if (status === "aborted") return "已中断";
|
||||||
if (status === "error") return "提交失败";
|
if (status === "error") return "提交失败";
|
||||||
if (status === "submitting") return "提交中";
|
if (status === "submitting") return "提交中";
|
||||||
return "等待确认";
|
return "等待确认";
|
||||||
@@ -109,6 +77,7 @@ const getPermissionStatusColor = (
|
|||||||
if (status === "approved_once") return approvedOncePermissionColor;
|
if (status === "approved_once") return approvedOncePermissionColor;
|
||||||
if (status === "approved_always") return theme.palette.success.main;
|
if (status === "approved_always") return theme.palette.success.main;
|
||||||
if (status === "rejected" || status === "error") return theme.palette.error.main;
|
if (status === "rejected" || status === "error") return theme.palette.error.main;
|
||||||
|
if (status === "aborted") return theme.palette.text.secondary;
|
||||||
return pendingPermissionColor;
|
return pendingPermissionColor;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -119,21 +88,24 @@ const getPermissionStatusTextColor = (
|
|||||||
if (status === "approved_once") return "#006c78";
|
if (status === "approved_once") return "#006c78";
|
||||||
if (status === "approved_always") return theme.palette.success.dark;
|
if (status === "approved_always") return theme.palette.success.dark;
|
||||||
if (status === "rejected" || status === "error") return theme.palette.error.main;
|
if (status === "rejected" || status === "error") return theme.palette.error.main;
|
||||||
|
if (status === "aborted") return theme.palette.text.secondary;
|
||||||
return "#8a5a00";
|
return "#8a5a00";
|
||||||
};
|
};
|
||||||
|
|
||||||
const PermissionRequestCard = ({
|
const PermissionRequestCard = ({
|
||||||
permission,
|
permission,
|
||||||
|
isRunning,
|
||||||
onReply,
|
onReply,
|
||||||
}: {
|
}: {
|
||||||
permission: NonNullable<Message["permissions"]>[number];
|
permission: NonNullable<Message["permissions"]>[number];
|
||||||
|
isRunning: boolean;
|
||||||
onReply: (requestId: string, reply: PermissionReply) => void;
|
onReply: (requestId: string, reply: PermissionReply) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isPending = permission.status === "pending" || permission.status === "error";
|
const isPending =
|
||||||
const isSubmitting = permission.status === "submitting";
|
isRunning && (permission.status === "pending" || permission.status === "error");
|
||||||
|
const isSubmitting = isRunning && permission.status === "submitting";
|
||||||
const primaryValue = getPermissionPrimaryValue(permission);
|
const primaryValue = getPermissionPrimaryValue(permission);
|
||||||
const metadataText = formatMetadata(permission.metadata);
|
|
||||||
const accentColor = getPermissionStatusColor(permission.status, theme);
|
const accentColor = getPermissionStatusColor(permission.status, theme);
|
||||||
const statusTextColor = getPermissionStatusTextColor(permission.status, theme);
|
const statusTextColor = getPermissionStatusTextColor(permission.status, theme);
|
||||||
const statusLabel = getPermissionStatusLabel(permission.status);
|
const statusLabel = getPermissionStatusLabel(permission.status);
|
||||||
@@ -231,12 +203,6 @@ const PermissionRequestCard = ({
|
|||||||
{primaryValue}
|
{primaryValue}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{metadataText ? (
|
|
||||||
<Typography variant="caption" color="text.secondary" sx={{ wordBreak: "break-word" }}>
|
|
||||||
{metadataText}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{permission.error ? (
|
{permission.error ? (
|
||||||
@@ -363,7 +329,13 @@ export const PermissionRequestGroup = ({
|
|||||||
const onceCount = permissions.filter((permission) => permission.status === "approved_once").length;
|
const onceCount = permissions.filter((permission) => permission.status === "approved_once").length;
|
||||||
const alwaysCount = permissions.filter((permission) => permission.status === "approved_always").length;
|
const alwaysCount = permissions.filter((permission) => permission.status === "approved_always").length;
|
||||||
const rejectedCount = permissions.filter((permission) => permission.status === "rejected").length;
|
const rejectedCount = permissions.filter((permission) => permission.status === "rejected").length;
|
||||||
const pendingCount = permissions.length - onceCount - alwaysCount - rejectedCount;
|
const abortedCount = permissions.filter((permission) => permission.status === "aborted").length;
|
||||||
|
const pendingCount = permissions.filter(
|
||||||
|
(permission) =>
|
||||||
|
permission.status === "pending" ||
|
||||||
|
permission.status === "submitting" ||
|
||||||
|
permission.status === "error",
|
||||||
|
).length;
|
||||||
const hasPendingPermissions = pendingCount > 0;
|
const hasPendingPermissions = pendingCount > 0;
|
||||||
const [expanded, setExpanded] = React.useState(false);
|
const [expanded, setExpanded] = React.useState(false);
|
||||||
const latestPermissions = permissions.slice(-3);
|
const latestPermissions = permissions.slice(-3);
|
||||||
@@ -378,9 +350,24 @@ export const PermissionRequestGroup = ({
|
|||||||
{ label: "允许一次", value: onceCount, color: getPermissionStatusColor("approved_once", theme), textColor: getPermissionStatusTextColor("approved_once", theme) },
|
{ label: "允许一次", value: onceCount, color: getPermissionStatusColor("approved_once", theme), textColor: getPermissionStatusTextColor("approved_once", theme) },
|
||||||
{ label: "始终允许", value: alwaysCount, color: getPermissionStatusColor("approved_always", theme), textColor: getPermissionStatusTextColor("approved_always", theme) },
|
{ label: "始终允许", value: alwaysCount, color: getPermissionStatusColor("approved_always", theme), textColor: getPermissionStatusTextColor("approved_always", theme) },
|
||||||
{ label: "拒绝", value: rejectedCount, color: getPermissionStatusColor("rejected", theme), textColor: getPermissionStatusTextColor("rejected", theme) },
|
{ label: "拒绝", value: rejectedCount, color: getPermissionStatusColor("rejected", theme), textColor: getPermissionStatusTextColor("rejected", theme) },
|
||||||
|
{ label: "中断", value: abortedCount, color: getPermissionStatusColor("aborted", theme), textColor: getPermissionStatusTextColor("aborted", theme) },
|
||||||
];
|
];
|
||||||
const chipColor = pendingCount > 0 ? getPermissionStatusColor("pending", theme) : rejectedCount > 0 ? getPermissionStatusColor("rejected", theme) : getPermissionStatusColor("approved_always", theme);
|
const chipColor =
|
||||||
const chipTextColor = pendingCount > 0 ? getPermissionStatusTextColor("pending", theme) : rejectedCount > 0 ? getPermissionStatusTextColor("rejected", theme) : getPermissionStatusTextColor("approved_always", theme);
|
pendingCount > 0
|
||||||
|
? getPermissionStatusColor("pending", theme)
|
||||||
|
: abortedCount > 0
|
||||||
|
? getPermissionStatusColor("aborted", theme)
|
||||||
|
: rejectedCount > 0
|
||||||
|
? getPermissionStatusColor("rejected", theme)
|
||||||
|
: getPermissionStatusColor("approved_always", theme);
|
||||||
|
const chipTextColor =
|
||||||
|
pendingCount > 0
|
||||||
|
? getPermissionStatusTextColor("pending", theme)
|
||||||
|
: abortedCount > 0
|
||||||
|
? getPermissionStatusTextColor("aborted", theme)
|
||||||
|
: rejectedCount > 0
|
||||||
|
? getPermissionStatusTextColor("rejected", theme)
|
||||||
|
: getPermissionStatusTextColor("approved_always", theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -549,12 +536,13 @@ export const PermissionRequestGroup = ({
|
|||||||
variant="caption"
|
variant="caption"
|
||||||
color="text.secondary"
|
color="text.secondary"
|
||||||
noWrap
|
noWrap
|
||||||
|
title={primaryValue}
|
||||||
sx={{
|
sx={{
|
||||||
display: "block",
|
display: "block",
|
||||||
fontFamily: permission.permission === "bash" ? "monospace" : undefined,
|
fontFamily: permission.permission === "bash" ? "monospace" : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{truncateText(primaryValue, 72)}
|
{primaryValue}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Chip
|
<Chip
|
||||||
@@ -591,6 +579,7 @@ export const PermissionRequestGroup = ({
|
|||||||
<PermissionRequestCard
|
<PermissionRequestCard
|
||||||
key={permission.requestId}
|
key={permission.requestId}
|
||||||
permission={permission}
|
permission={permission}
|
||||||
|
isRunning={isRunning}
|
||||||
onReply={onReply}
|
onReply={onReply}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -605,6 +594,7 @@ export const PermissionRequestGroup = ({
|
|||||||
<PermissionRequestCard
|
<PermissionRequestCard
|
||||||
key={permission.requestId}
|
key={permission.requestId}
|
||||||
permission={permission}
|
permission={permission}
|
||||||
|
isRunning={isRunning}
|
||||||
onReply={onReply}
|
onReply={onReply}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -613,5 +603,3 @@ export const PermissionRequestGroup = ({
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import StopRounded from "@mui/icons-material/StopRounded";
|
|||||||
|
|
||||||
type AgentTurnProps = {
|
type AgentTurnProps = {
|
||||||
message: Message;
|
message: Message;
|
||||||
|
isStreaming: boolean;
|
||||||
messageSpeechState: SpeechState;
|
messageSpeechState: SpeechState;
|
||||||
onSpeak: (messageId: string, text: string) => void;
|
onSpeak: (messageId: string, text: string) => void;
|
||||||
onPause: () => void;
|
onPause: () => void;
|
||||||
@@ -54,6 +55,7 @@ type AgentTurnProps = {
|
|||||||
export const AgentTurn = React.memo(
|
export const AgentTurn = React.memo(
|
||||||
({
|
({
|
||||||
message,
|
message,
|
||||||
|
isStreaming,
|
||||||
messageSpeechState,
|
messageSpeechState,
|
||||||
onSpeak,
|
onSpeak,
|
||||||
onPause,
|
onPause,
|
||||||
@@ -277,7 +279,7 @@ export const AgentTurn = React.memo(
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{isHovered && (
|
{isHovered && !isStreaming && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.9, y: 5 }}
|
initial={{ opacity: 0, scale: 0.9, y: 5 }}
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type AgentWorkspaceProps = {
|
|||||||
|
|
||||||
type TurnListProps = {
|
type TurnListProps = {
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
|
isStreaming: boolean;
|
||||||
speakingMessageId: string | null;
|
speakingMessageId: string | null;
|
||||||
speechState: SpeechState;
|
speechState: SpeechState;
|
||||||
onSpeak: (messageId: string, text: string) => void;
|
onSpeak: (messageId: string, text: string) => void;
|
||||||
@@ -55,6 +56,7 @@ const sameMessages = (left: Message[], right: Message[]) =>
|
|||||||
|
|
||||||
const TurnListInner = ({
|
const TurnListInner = ({
|
||||||
messages,
|
messages,
|
||||||
|
isStreaming,
|
||||||
speakingMessageId,
|
speakingMessageId,
|
||||||
speechState,
|
speechState,
|
||||||
onSpeak,
|
onSpeak,
|
||||||
@@ -73,6 +75,7 @@ const TurnListInner = ({
|
|||||||
<AgentTurn
|
<AgentTurn
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
|
isStreaming={isStreaming}
|
||||||
messageSpeechState={speakingMessageId === message.id ? speechState : "idle"}
|
messageSpeechState={speakingMessageId === message.id ? speechState : "idle"}
|
||||||
onSpeak={onSpeak}
|
onSpeak={onSpeak}
|
||||||
onPause={onPauseSpeech}
|
onPause={onPauseSpeech}
|
||||||
@@ -93,6 +96,7 @@ const TurnList = React.memo(
|
|||||||
TurnListInner,
|
TurnListInner,
|
||||||
(prevProps, nextProps) =>
|
(prevProps, nextProps) =>
|
||||||
sameMessages(prevProps.messages, nextProps.messages) &&
|
sameMessages(prevProps.messages, nextProps.messages) &&
|
||||||
|
prevProps.isStreaming === nextProps.isStreaming &&
|
||||||
prevProps.speakingMessageId === nextProps.speakingMessageId &&
|
prevProps.speakingMessageId === nextProps.speakingMessageId &&
|
||||||
prevProps.speechState === nextProps.speechState &&
|
prevProps.speechState === nextProps.speechState &&
|
||||||
prevProps.onSpeak === nextProps.onSpeak &&
|
prevProps.onSpeak === nextProps.onSpeak &&
|
||||||
@@ -274,6 +278,7 @@ export const AgentWorkspace = ({
|
|||||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||||
<TurnList
|
<TurnList
|
||||||
messages={historyMessages}
|
messages={historyMessages}
|
||||||
|
isStreaming={isStreaming}
|
||||||
speakingMessageId={speakingMessageId}
|
speakingMessageId={speakingMessageId}
|
||||||
speechState={speechState}
|
speechState={speechState}
|
||||||
onSpeak={onSpeak}
|
onSpeak={onSpeak}
|
||||||
@@ -290,6 +295,7 @@ export const AgentWorkspace = ({
|
|||||||
{streamingMessage ? (
|
{streamingMessage ? (
|
||||||
<TurnList
|
<TurnList
|
||||||
messages={[streamingMessage]}
|
messages={[streamingMessage]}
|
||||||
|
isStreaming={isStreaming}
|
||||||
speakingMessageId={speakingMessageId}
|
speakingMessageId={speakingMessageId}
|
||||||
speechState={speechState}
|
speechState={speechState}
|
||||||
onSpeak={onSpeak}
|
onSpeak={onSpeak}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export type AgentPermissionStatus =
|
|||||||
| "approved_once"
|
| "approved_once"
|
||||||
| "approved_always"
|
| "approved_always"
|
||||||
| "rejected"
|
| "rejected"
|
||||||
|
| "aborted"
|
||||||
| "error";
|
| "error";
|
||||||
|
|
||||||
export type AgentPermissionRequest = {
|
export type AgentPermissionRequest = {
|
||||||
@@ -40,7 +41,7 @@ export type AgentPermissionRequest = {
|
|||||||
sessionId: string;
|
sessionId: string;
|
||||||
permission: string;
|
permission: string;
|
||||||
patterns: string[];
|
patterns: string[];
|
||||||
metadata: Record<string, unknown>;
|
target?: string;
|
||||||
always: string[];
|
always: string[];
|
||||||
tool?: {
|
tool?: {
|
||||||
messageID: string;
|
messageID: string;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { cloneMessage } from "./GlobalChatbox.utils";
|
||||||
|
import type { Message } from "./GlobalChatbox.types";
|
||||||
|
|
||||||
|
describe("cloneMessage", () => {
|
||||||
|
it("normalizes persisted question and todo arrays", () => {
|
||||||
|
const message = {
|
||||||
|
id: "assistant-1",
|
||||||
|
role: "assistant",
|
||||||
|
content: "需要补充信息",
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
requestId: "question-1",
|
||||||
|
sessionId: "session-1",
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
header: "范围",
|
||||||
|
question: "请选择分析范围",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
createdAt: 1,
|
||||||
|
status: "pending",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
todos: {
|
||||||
|
sessionId: "session-1",
|
||||||
|
createdAt: 1,
|
||||||
|
},
|
||||||
|
} as unknown as Message;
|
||||||
|
|
||||||
|
const cloned = cloneMessage(message);
|
||||||
|
|
||||||
|
expect(cloned.questions?.[0]?.questions[0]?.options).toEqual([]);
|
||||||
|
expect(cloned.todos?.todos).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { Message } from "./GlobalChatbox.types";
|
import type { Message } from "./GlobalChatbox.types";
|
||||||
|
import type {
|
||||||
|
AgentQuestionRequest,
|
||||||
|
AgentTodoUpdate,
|
||||||
|
} from "@/lib/chatStream";
|
||||||
|
|
||||||
export const createId = () =>
|
export const createId = () =>
|
||||||
`${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
`${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
@@ -29,10 +33,65 @@ export const stripMarkdown = (md: string): string =>
|
|||||||
.replace(/<[^>]+>/g, "")
|
.replace(/<[^>]+>/g, "")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
const normalizeQuestionRequests = (
|
||||||
|
questions: Message["questions"],
|
||||||
|
): Message["questions"] =>
|
||||||
|
Array.isArray(questions)
|
||||||
|
? questions.map((request) => ({
|
||||||
|
...request,
|
||||||
|
questions: Array.isArray(request.questions)
|
||||||
|
? request.questions.map((question) => ({
|
||||||
|
...question,
|
||||||
|
header: typeof question.header === "string" ? question.header : "",
|
||||||
|
question:
|
||||||
|
typeof question.question === "string" ? question.question : "",
|
||||||
|
options: Array.isArray(question.options)
|
||||||
|
? question.options.map((option) => ({
|
||||||
|
label:
|
||||||
|
typeof option.label === "string" ? option.label : "",
|
||||||
|
description:
|
||||||
|
typeof option.description === "string"
|
||||||
|
? option.description
|
||||||
|
: "",
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
|
answers: Array.isArray(request.answers)
|
||||||
|
? request.answers.map((answer) =>
|
||||||
|
Array.isArray(answer)
|
||||||
|
? answer.filter((item): item is string => typeof item === "string")
|
||||||
|
: [],
|
||||||
|
)
|
||||||
|
: undefined,
|
||||||
|
} satisfies AgentQuestionRequest))
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const normalizeTodoUpdate = (todos: Message["todos"]): Message["todos"] => {
|
||||||
|
if (!todos) return undefined;
|
||||||
|
return {
|
||||||
|
...todos,
|
||||||
|
todos: Array.isArray(todos.todos)
|
||||||
|
? todos.todos.map((todo) => ({ ...todo }))
|
||||||
|
: [],
|
||||||
|
} satisfies AgentTodoUpdate;
|
||||||
|
};
|
||||||
|
|
||||||
export const cloneMessage = (message: Message): Message => ({
|
export const cloneMessage = (message: Message): Message => ({
|
||||||
...message,
|
...message,
|
||||||
progress: message.progress ? [...message.progress] : undefined,
|
progress: Array.isArray(message.progress) ? [...message.progress] : undefined,
|
||||||
artifacts: message.artifacts ? [...message.artifacts] : undefined,
|
artifacts: Array.isArray(message.artifacts) ? [...message.artifacts] : undefined,
|
||||||
|
permissions: Array.isArray(message.permissions)
|
||||||
|
? message.permissions.map((permission) => ({
|
||||||
|
...permission,
|
||||||
|
patterns: Array.isArray(permission.patterns)
|
||||||
|
? [...permission.patterns]
|
||||||
|
: [],
|
||||||
|
always: Array.isArray(permission.always) ? [...permission.always] : [],
|
||||||
|
}))
|
||||||
|
: undefined,
|
||||||
|
questions: normalizeQuestionRequests(message.questions),
|
||||||
|
todos: normalizeTodoUpdate(message.todos),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const cloneMessages = (messages: Message[]) => messages.map(cloneMessage);
|
export const cloneMessages = (messages: Message[]) => messages.map(cloneMessage);
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const upsertPermission = (
|
|||||||
sessionId: event.sessionId,
|
sessionId: event.sessionId,
|
||||||
permission: event.permission,
|
permission: event.permission,
|
||||||
patterns: event.patterns,
|
patterns: event.patterns,
|
||||||
metadata: event.metadata,
|
target: event.target,
|
||||||
always: event.always,
|
always: event.always,
|
||||||
tool: event.tool,
|
tool: event.tool,
|
||||||
createdAt: event.createdAt,
|
createdAt: event.createdAt,
|
||||||
@@ -364,7 +364,7 @@ export const normalizeSessionTodos = (
|
|||||||
return changed ? nextMessages : messages;
|
return changed ? nextMessages : messages;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rejectOpenPermissionsAfterAbort = (
|
export const abortOpenPermissionsAfterAbort = (
|
||||||
permissions: AgentPermissionRequest[] | undefined,
|
permissions: AgentPermissionRequest[] | undefined,
|
||||||
) => {
|
) => {
|
||||||
if (!permissions?.length) return permissions;
|
if (!permissions?.length) return permissions;
|
||||||
@@ -380,7 +380,7 @@ export const rejectOpenPermissionsAfterAbort = (
|
|||||||
changed = true;
|
changed = true;
|
||||||
return {
|
return {
|
||||||
...permission,
|
...permission,
|
||||||
status: "rejected" as const,
|
status: "aborted" as const,
|
||||||
repliedAt: Date.now(),
|
repliedAt: Date.now(),
|
||||||
error: undefined,
|
error: undefined,
|
||||||
};
|
};
|
||||||
@@ -415,12 +415,12 @@ export const rejectOpenQuestionsAfterAbort = (
|
|||||||
export const finalizeAssistantMessageAfterAbort = (message: Message): Message => {
|
export const finalizeAssistantMessageAfterAbort = (message: Message): Message => {
|
||||||
const completedProgress = completeRunningProgress(message.progress);
|
const completedProgress = completeRunningProgress(message.progress);
|
||||||
const cancelledTodos = cancelRunningTodos(message.todos);
|
const cancelledTodos = cancelRunningTodos(message.todos);
|
||||||
const rejectedPermissions = rejectOpenPermissionsAfterAbort(message.permissions);
|
const abortedPermissions = abortOpenPermissionsAfterAbort(message.permissions);
|
||||||
const rejectedQuestions = rejectOpenQuestionsAfterAbort(message.questions);
|
const rejectedQuestions = rejectOpenQuestionsAfterAbort(message.questions);
|
||||||
const hasVisibleOutput =
|
const hasVisibleOutput =
|
||||||
message.content.trim().length > 0 ||
|
message.content.trim().length > 0 ||
|
||||||
Boolean(message.artifacts?.length) ||
|
Boolean(message.artifacts?.length) ||
|
||||||
Boolean(rejectedPermissions?.length) ||
|
Boolean(abortedPermissions?.length) ||
|
||||||
Boolean(rejectedQuestions?.length) ||
|
Boolean(rejectedQuestions?.length) ||
|
||||||
Boolean(completedProgress?.length) ||
|
Boolean(completedProgress?.length) ||
|
||||||
Boolean(cancelledTodos);
|
Boolean(cancelledTodos);
|
||||||
@@ -434,7 +434,7 @@ export const finalizeAssistantMessageAfterAbort = (message: Message): Message =>
|
|||||||
content: message.content || "⚠️ **请求已中断**",
|
content: message.content || "⚠️ **请求已中断**",
|
||||||
isError: true,
|
isError: true,
|
||||||
progress: completedProgress,
|
progress: completedProgress,
|
||||||
permissions: rejectedPermissions,
|
permissions: abortedPermissions,
|
||||||
questions: rejectedQuestions,
|
questions: rejectedQuestions,
|
||||||
todos: cancelledTodos,
|
todos: cancelledTodos,
|
||||||
};
|
};
|
||||||
@@ -454,4 +454,3 @@ export const createAssistantMessage = (): Message => ({
|
|||||||
role: "assistant",
|
role: "assistant",
|
||||||
content: "",
|
content: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ describe("useAgentChatSession actions", () => {
|
|||||||
requestId: "perm-1",
|
requestId: "perm-1",
|
||||||
permission: "bash",
|
permission: "bash",
|
||||||
patterns: ["rm *"],
|
patterns: ["rm *"],
|
||||||
metadata: { command: "rm tmp.txt" },
|
target: "rm tmp.txt",
|
||||||
always: ["rm *"],
|
always: ["rm *"],
|
||||||
createdAt: 123,
|
createdAt: 123,
|
||||||
});
|
});
|
||||||
@@ -163,7 +163,7 @@ describe("useAgentChatSession actions", () => {
|
|||||||
requestId: "perm-abort",
|
requestId: "perm-abort",
|
||||||
permission: "bash",
|
permission: "bash",
|
||||||
patterns: ["npm test"],
|
patterns: ["npm test"],
|
||||||
metadata: { command: "npm test" },
|
target: "npm test",
|
||||||
always: ["npm test"],
|
always: ["npm test"],
|
||||||
createdAt: 1002,
|
createdAt: 1002,
|
||||||
} satisfies StreamEvent);
|
} satisfies StreamEvent);
|
||||||
@@ -238,7 +238,7 @@ describe("useAgentChatSession actions", () => {
|
|||||||
permissions: [
|
permissions: [
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
requestId: "perm-abort",
|
requestId: "perm-abort",
|
||||||
status: "rejected",
|
status: "aborted",
|
||||||
repliedAt: expect.any(Number),
|
repliedAt: expect.any(Number),
|
||||||
error: undefined,
|
error: undefined,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ describe("streamAgentChat", () => {
|
|||||||
apiFetch.mockResolvedValue({
|
apiFetch.mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
body: makeStream([
|
body: makeStream([
|
||||||
'event: permission_request\ndata: {"session_id":"s1","request_id":"perm-1","permission":"bash","patterns":["rm *"],"metadata":{"command":"rm tmp.txt"},"always":["rm *"],"created_at":123}\n\n',
|
'event: permission_request\ndata: {"session_id":"s1","request_id":"perm-1","permission":"bash","patterns":["rm *"],"target":"rm tmp.txt","always":["rm *"],"created_at":123}\n\n',
|
||||||
'event: permission_response\ndata: {"session_id":"s1","request_id":"perm-1","reply":"reject"}\n\n',
|
'event: permission_response\ndata: {"session_id":"s1","request_id":"perm-1","reply":"reject"}\n\n',
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
@@ -205,7 +205,7 @@ describe("streamAgentChat", () => {
|
|||||||
requestId: "perm-1",
|
requestId: "perm-1",
|
||||||
permission: "bash",
|
permission: "bash",
|
||||||
patterns: ["rm *"],
|
patterns: ["rm *"],
|
||||||
metadata: { command: "rm tmp.txt" },
|
target: "rm tmp.txt",
|
||||||
always: ["rm *"],
|
always: ["rm *"],
|
||||||
tool: undefined,
|
tool: undefined,
|
||||||
createdAt: 123,
|
createdAt: 123,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export type StreamEvent =
|
|||||||
requestId: string;
|
requestId: string;
|
||||||
permission: string;
|
permission: string;
|
||||||
patterns: string[];
|
patterns: string[];
|
||||||
metadata: Record<string, unknown>;
|
target?: string;
|
||||||
always: string[];
|
always: string[];
|
||||||
tool?: {
|
tool?: {
|
||||||
messageID: string;
|
messageID: string;
|
||||||
@@ -296,7 +296,7 @@ const emitParsedStreamEvent = (
|
|||||||
request_id?: string;
|
request_id?: string;
|
||||||
permission?: string;
|
permission?: string;
|
||||||
patterns?: unknown;
|
patterns?: unknown;
|
||||||
metadata?: unknown;
|
target?: string;
|
||||||
always?: unknown;
|
always?: unknown;
|
||||||
created_at?: number;
|
created_at?: number;
|
||||||
reply?: PermissionReply;
|
reply?: PermissionReply;
|
||||||
@@ -370,7 +370,7 @@ const emitParsedStreamEvent = (
|
|||||||
patterns: Array.isArray(parsed.patterns)
|
patterns: Array.isArray(parsed.patterns)
|
||||||
? parsed.patterns.filter((item): item is string => typeof item === "string")
|
? parsed.patterns.filter((item): item is string => typeof item === "string")
|
||||||
: [],
|
: [],
|
||||||
metadata: isObjectRecord(parsed.metadata) ? parsed.metadata : {},
|
target: typeof parsed.target === "string" ? parsed.target : undefined,
|
||||||
always: Array.isArray(parsed.always)
|
always: Array.isArray(parsed.always)
|
||||||
? parsed.always.filter((item): item is string => typeof item === "string")
|
? parsed.always.filter((item): item is string => typeof item === "string")
|
||||||
: [],
|
: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user