feat: add permission request UI
Build Push and Deploy / docker-image (push) Successful in 1m2s
Build Push and Deploy / deploy-fallback-log (push) Has been skipped

This commit is contained in:
2026-06-08 13:32:50 +08:00
parent 5fc1812d53
commit e32823e4b5
9 changed files with 999 additions and 12 deletions
@@ -5,12 +5,14 @@ import { useCallback, useEffect, useRef, useState } from "react";
import {
abortAgentChat,
forkAgentChat,
replyAgentPermission,
resumeAgentChatStream,
streamAgentChat,
} from "@/lib/chatStream";
import type { AgentModel, StreamEvent } from "@/lib/chatStream";
import type { AgentModel, PermissionReply, StreamEvent } from "@/lib/chatStream";
import type {
AgentArtifact,
AgentPermissionRequest,
BranchGroup,
BranchTransition,
ChatProgress,
@@ -130,6 +132,41 @@ const completeRunningProgress = (progress: ChatProgress[] | undefined) =>
};
});
const upsertPermission = (
permissions: AgentPermissionRequest[] | undefined,
event: StreamEvent & { type: "permission_request" },
) => {
const next = [...(permissions ?? [])];
const index = next.findIndex((item) => item.requestId === event.requestId);
const nextItem: AgentPermissionRequest = {
requestId: event.requestId,
sessionId: event.sessionId,
permission: event.permission,
patterns: event.patterns,
metadata: event.metadata,
always: event.always,
tool: event.tool,
createdAt: event.createdAt,
status: "pending",
};
if (index >= 0) {
next[index] = {
...next[index],
...nextItem,
status: next[index].status === "submitting" ? "submitting" : nextItem.status,
};
} else {
next.push(nextItem);
}
return next;
};
const toPermissionStatus = (reply: PermissionReply): AgentPermissionRequest["status"] => {
if (reply === "always") return "approved_always";
if (reply === "once") return "approved_once";
return "rejected";
};
const finalizeAssistantMessageAfterAbort = (message: Message): Message => {
const completedProgress = completeRunningProgress(message.progress);
const hasVisibleOutput =
@@ -442,6 +479,38 @@ export const useAgentChatSession = ({
assistantMessageId,
appendArtifact,
});
} else if (event.type === "permission_request") {
setMessages((prev) =>
prev.map((message) =>
message.id === assistantMessageId
? {
...message,
permissions: upsertPermission(message.permissions, event),
}
: message,
),
);
} else if (event.type === "permission_response") {
setMessages((prev) =>
prev.map((message) => {
if (message.id !== assistantMessageId || !message.permissions?.length) {
return message;
}
return {
...message,
permissions: message.permissions.map((permission) =>
permission.requestId === event.requestId
? {
...permission,
status: toPermissionStatus(event.reply),
repliedAt: Date.now(),
error: undefined,
}
: permission,
),
};
}),
);
} else if (event.type === "session_title") {
const nextTitle = event.title.trim();
if (nextTitle && !isSessionTitleManuallyEditedRef.current) {
@@ -647,6 +716,75 @@ export const useAgentChatSession = ({
cancelPromiseRef.current = trackedCancelPromise;
}, [getLastAssistantMessageId]);
const replyPermission = useCallback(
async (requestId: string, reply: PermissionReply) => {
const target = messagesRef.current
.flatMap((message) => message.permissions ?? [])
.find((permission) => permission.requestId === requestId);
if (!target || target.status === "submitting") {
return;
}
setMessages((prev) =>
prev.map((message) =>
!message.permissions?.some((permission) => permission.requestId === requestId)
? message
: {
...message,
permissions: message.permissions.map((permission) =>
permission.requestId === requestId
? { ...permission, status: "submitting", error: undefined }
: permission,
),
},
),
);
try {
await replyAgentPermission(target.sessionId, requestId, reply);
setMessages((prev) =>
prev.map((message) =>
!message.permissions?.some((permission) => permission.requestId === requestId)
? message
: {
...message,
permissions: message.permissions.map((permission) =>
permission.requestId === requestId
? {
...permission,
status: toPermissionStatus(reply),
repliedAt: Date.now(),
error: undefined,
}
: permission,
),
},
),
);
} catch (error) {
setMessages((prev) =>
prev.map((message) =>
!message.permissions?.some((permission) => permission.requestId === requestId)
? message
: {
...message,
permissions: message.permissions.map((permission) =>
permission.requestId === requestId
? {
...permission,
status: "error",
error: error instanceof Error ? error.message : String(error),
}
: permission,
),
},
),
);
}
},
[],
);
const createSession = useCallback(() => {
if (isHydrating || isStreaming) return;
@@ -982,6 +1120,7 @@ export const useAgentChatSession = ({
editAndResubmit,
cycleBranch,
abort,
replyPermission,
createSession,
renameSession,
removeSession,